Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Model Zoo #166

Merged
merged 21 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f3ebd0e
ci: bump up to 0.10 & install pre&rc in ci
furiosamg Aug 10, 2023
1a1308c
ci: use npu in default tests too
furiosamg Aug 10, 2023
b3d60dd
ci: preload libgomp at very best effort
furiosamg Aug 10, 2023
e7913fd
ci: install legacy furiosa-libcompiler too for now
furiosamg Aug 10, 2023
11279a9
nits: update pyproject & loosen ver constraints
furiosamg Aug 10, 2023
4fdd527
feat: refactor `furiosa.models.types.Model` class
furiosamg Aug 10, 2023
bbe3a81
fix: remove .numpy() call for tensors
furiosamg Aug 10, 2023
0632b40
fix: upgrade enf_generator to accept 0.10.0
furiosamg Aug 10, 2023
789a7ba
fix: use isinstance over direct type comparison
furiosamg Aug 10, 2023
dd9053b
feat: add generated 0.10.0 artifacts
furiosamg Aug 10, 2023
2841633
fix: use not-quantized preproc for onnx example
furiosamg Aug 10, 2023
f2ba5ad
fix: update ssd_resnet34 example and run in ci
furiosamg Aug 10, 2023
055168b
fix: use Runner API over deprecated Session API
furiosamg Aug 10, 2023
9ef6644
fix: add image open helper, assert uint8 dtype
furiosamg Aug 10, 2023
1339aac
fix: refactor postprocess type
furiosamg Aug 10, 2023
8ed435a
fix: address review from @senokay
furiosamg Aug 10, 2023
105e280
fix: invert with_quantize to skip_quantize
furiosamg Aug 10, 2023
bcd8f03
fix: raise FileNotFound with proper Path
furiosamg Aug 10, 2023
b850c47
fix: address reviews from @senokay
furiosamg Aug 11, 2023
dc0aecc
fix: reflect review by @libc-furiosa
furiosamg Aug 14, 2023
cad487e
docs: rename & add explanations about with_scaling
furiosamg Aug 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SHELL=/bin/bash -o pipefail

ONNXRUNTIME_VERSION := 1.13.1-?
TOOLCHAIN_VERSION := 0.9.1-?
ONNXRUNTIME_VERSION := 1.15.1-?
TOOLCHAIN_VERSION := 0.10.0-?
LIBHAL_VERSION := 0.11.0-?

.PHONY: check-docker-tag toolchain lint test unit_tests examples regression-test-all \
Expand All @@ -15,8 +15,11 @@ ifndef DOCKER_TAG
endif

toolchain:
apt-get update
apt-get install -y --allow-downgrades libonnxruntime=$(ONNXRUNTIME_VERSION)
apt-get install -y --allow-downgrades furiosa-libcompiler=$(TOOLCHAIN_VERSION) furiosa-libnux=$(TOOLCHAIN_VERSION)
apt-get install -y --allow-downgrades furiosa-compiler=$(TOOLCHAIN_VERSION)
# TODO: remove me when possible
apt-get install -y --allow-downgrades furiosa-libcompiler=$(TOOLCHAIN_VERSION)
apt-get install -y --allow-downgrades furiosa-libhal-warboy=$(LIBHAL_VERSION)

lint:
Expand All @@ -31,7 +34,7 @@ unit_tests:
pytest ./tests/unit/ -s

examples:
for f in $$(ls docs/examples/*.py | grep -v "ssd_resnet34"); do echo"";echo "[TEST] $$f ..."; python3 $$f || exit 1; done
for f in $$(find docs/examples/ -name *.py); do printf "\n[TEST] $$f ...\n"; python3 $$f || exit 1; done

regression-test-all:
pytest ./tests/bench/
Expand Down
5 changes: 5 additions & 0 deletions ci-constraints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# This pip constraints file is for the reproducibility of model accuracies
opencv-python-headless==4.8.0.76
torch==2.0.1
torchvision==0.15.2
numpy==1.25.2
4 changes: 3 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ RUN pip3 install --upgrade pip wheel setuptools Cython pytest pycocotools \

RUN echo "deb [arch=amd64] https://internal-archive.furiosa.dev/ubuntu focal restricted" \
> /etc/apt/sources.list.d/furiosa.list && \
echo "deb [arch=amd64] https://internal-archive.furiosa.dev/ubuntu focal-rc restricted" \
>> /etc/apt/sources.list.d/furiosa.list && \
echo "deb [arch=amd64] https://internal-archive.furiosa.dev/ubuntu focal-nightly restricted" \
>> /etc/apt/sources.list.d/furiosa.list

Expand All @@ -31,4 +33,4 @@ RUN --mount=type=secret,id=furiosa.conf,dst=/etc/apt/auth.conf.d/furiosa.conf,re
apt-get update && \
make toolchain
RUN --mount=type=secret,id=.netrc,dst=/root/.netrc,required \
pip install --extra-index-url https://internal-pypi.furiosa.dev/simple .[test]
pip install --pre --extra-index-url https://internal-pypi.furiosa.dev/simple --constraint ci-constraints.txt .[test]
8 changes: 4 additions & 4 deletions docs/examples/efficientnet_b0.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from furiosa.models.vision import EfficientNetB0
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

image = "tests/assets/cat.jpg"

effnetb0 = EfficientNetB0.load()
with session.create(effnetb0.enf) as sess:
effnetb0 = EfficientNetB0()
with create_runner(effnetb0.model_source()) as runner:
inputs, _ = effnetb0.preprocess(image)
outputs = sess.run(inputs).numpy()
outputs = runner.run(inputs)
effnetb0.postprocess(outputs)
8 changes: 4 additions & 4 deletions docs/examples/efficientnet_v2_s.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from furiosa.models.vision import EfficientNetV2s
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

image = "tests/assets/cat.jpg"

effnetv2s = EfficientNetV2s.load()
with session.create(effnetv2s.enf) as sess:
effnetv2s = EfficientNetV2s()
with create_runner(effnetv2s.model_source()) as runner:
inputs, _ = effnetv2s.preprocess(image)
outputs = sess.run(inputs).numpy()
outputs = runner.run(inputs)
effnetv2s.postprocess(outputs)
2 changes: 1 addition & 1 deletion docs/examples/loading_model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from furiosa.models.types import Model
from furiosa.models.vision import ResNet50

model: Model = ResNet50.load()
model: Model = ResNet50()
8 changes: 4 additions & 4 deletions docs/examples/resnet50.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from furiosa.models.vision import ResNet50
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

image = "tests/assets/cat.jpg"

resnet50 = ResNet50.load()
with session.create(resnet50.enf) as sess:
resnet50 = ResNet50()
with create_runner(resnet50.model_source()) as runner:
inputs, _ = resnet50.preprocess(image)
outputs = sess.run(inputs).numpy()
outputs = runner.run(inputs)
resnet50.postprocess(outputs)
8 changes: 4 additions & 4 deletions docs/examples/ssd_mobilenet.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from furiosa.models.vision import SSDMobileNet
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

image = ["tests/assets/cat.jpg"]

mobilenet = SSDMobileNet.load()
with session.create(mobilenet.enf) as sess:
mobilenet = SSDMobileNet()
with create_runner(mobilenet.model_source()) as create_runner:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 코드들과 일관성을 맞추면 좋을 것 같습니다.

Suggested change
with create_runner(mobilenet.model_source()) as create_runner:
with create_runner(mobilenet.model_source()) as runner:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dc0aecc 에서 반영했습니다.

inputs, contexts = mobilenet.preprocess(image)
outputs = sess.run(inputs).numpy()
outputs = create_runner.run(inputs)
mobilenet.postprocess(outputs, contexts[0])
8 changes: 4 additions & 4 deletions docs/examples/ssd_mobilenet_native.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from furiosa.models.vision import SSDMobileNet
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

image = ["tests/assets/cat.jpg"]

mobilenet = SSDMobileNet.load(use_native=True)
with session.create(mobilenet.enf) as sess:
mobilenet = SSDMobileNet(postprocessor_type="Rust")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postprocessor_type의 타입을 보니 Union[str, Platform]로 되어있는데요. 값을 str 타입으로 쓰고있는 이유가 무엇일까요? Platform을 쓰는 것이 좋을 것 같은데, 특별한 이유가 있었을 것 같기도 해서요.

Copy link
Collaborator Author

@furiosamg furiosamg Aug 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platform 이 strEnum 이기도 하고, 사용자가 편하게 문자열로 선택하면 좋을 것 같아서 그렇게 했습니다. 사실 strict 하게 typing 하자면 Platform만 주는 것이 맞긴 한데요, 모델 주는 사용자 편의성이 중요한 프로젝트인 것 같아서 임의로 그렇게 했습니다. 아님 혹시 더 좋은 방법이 있을까요?

with create_runner(mobilenet.model_source()) as runner:
inputs, contexts = mobilenet.preprocess(image)
outputs = sess.run(inputs).numpy()
outputs = runner.run(inputs)
mobilenet.postprocess(outputs, contexts[0])
18 changes: 8 additions & 10 deletions docs/examples/ssd_mobilenet_onnx.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import yaml

from furiosa.models.vision import SSDMobileNet
from furiosa.quantizer import quantize
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

compiler_config = {"lower_tabulated_dequantize": True}

image = ["tests/assets/cat.jpg"]

mobilenet = SSDMobileNet.load()
onnx_model: bytes = mobilenet.source
calib_range: dict = yaml.full_load(mobilenet.calib_yaml)
mobilenet = SSDMobileNet()
onnx_model: bytes = mobilenet.origin
calib_range: dict = mobilenet.tensor_name_to_range

# See https://furiosa-ai.github.io/docs/latest/en/api/python/furiosa.quantizer.html#furiosa.quantizer.quantize
# for more details
dfg = quantize(onnx_model, calib_range, with_quantize=False)
quantized_onnx = quantize(onnx_model, calib_range)

with session.create(dfg, compiler_config=compiler_config) as sess:
inputs, contexts = mobilenet.preprocess(image)
outputs = sess.run(inputs).numpy()
with create_runner(quantized_onnx, compiler_config=compiler_config) as runner:
inputs, contexts = mobilenet.preprocess(image, skip_quantize=False)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 커멘트는 가볍게 드리는 의견입니다. 다른 분들도 견해를 들려주시면 좋을 것 같습니다.

기존의 with_quantize나 이번의 skip_quantize 라는 이름을 보았을 때,
quantize를 왜 해야하는지? 언제 skip 하는지? 어떤 효과를 주는지? 마음대로 설정할 수 있는지? 에 대한 의문이 생깁니다.
context를 아는 경우에는 조금만 생각해보면 알 수 있지만, 아닌 일반 사용자는 쉽게 이해할 수 있을지 모르겠네요.

저는 모델의 입력 텐서 타입이 float인지 int(uint)인지가 중요하지 않을까, 하고 생각이 들었는데요,
float 라면 image를 read한 값을 형변환을 할 것이고, uint라면 변환을 생략할 것이기 때문입니다.
결과적으로 quantize 여부는 타입에 의존적이게 되므로, 이를 인자로 True/False로 지정하여 선택이 가능한 것 처럼 보이기보다는
모델이 어떤 타입인지를 표현하는 인자가 되면 보다 설명이 간단해지지 않을까 싶습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cad487e 에서 with_scaling이라는 파라미터로 바꾸었고 설명을 추가했습니다. 구두로 논의 나눈 내용이라 자세히 적지는 않겠습니다. 감사합니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모델이 어떤 타입인지를 표현하는 인자가 되면 보다 설명이 간단해지지 않을까 싶습니다.

이 PR 이 크기 때문에 이 관련해서는 별도 PR 로 다루면 좋을 것 같기는 합니다만, 조금 설명을 듣고 싶은데요. with_scaling 이라는 이름이 여전히 모호한 느낌이 있는데요. 제가 인용한 병찬님이 말씀 처럼 use_fp32 = True 같은 옵션이나 dtype을 직접 넣는 것을 고려하지 않았는지 궁금하기도 합니다.

outputs = runner.run(inputs)
mobilenet.postprocess(outputs, contexts[0])
8 changes: 4 additions & 4 deletions docs/examples/ssd_resnet34.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from furiosa.models.vision import SSDResNet34
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

resnet34 = SSDResNet34.load()
resnet34 = SSDResNet34(postprocessor_type="Python")

with session.create(resnet34.enf) as sess:
with create_runner(resnet34.model_source()) as runner:
image, contexts = resnet34.preprocess(["tests/assets/cat.jpg"])
output = sess.run(image).numpy()
output = runner.run(image)
resnet34.postprocess(output, contexts=contexts)
8 changes: 4 additions & 4 deletions docs/examples/ssd_resnet34_native.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from furiosa.models.vision import SSDResNet34
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

resnet34 = SSDResNet34.load(use_native=True)
resnet34 = SSDResNet34(postprocessor_type="Rust")

with session.create(resnet34.enf) as sess:
with create_runner(resnet34.model_source()) as runner:
image, contexts = resnet34.preprocess(["tests/assets/cat.jpg"])
output = sess.run(image).numpy()
output = runner.run(image)
resnet34.postprocessor(output, contexts=contexts[0])
8 changes: 4 additions & 4 deletions docs/examples/yolov5l.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import numpy as np

from furiosa.models.vision import YOLOv5l
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

yolov5l = YOLOv5l.load()
yolov5l = YOLOv5l()

with session.create(yolov5l.enf) as sess:
with create_runner(yolov5l.model_source()) as runner:
image = cv2.imread("tests/assets/yolov5-test.jpg")
inputs, contexts = yolov5l.preprocess([image])
output = sess.run(np.expand_dims(inputs[0], axis=0)).numpy()
output = runner.run(np.expand_dims(inputs[0], axis=0))
yolov5l.postprocess(output, contexts=contexts)
8 changes: 4 additions & 4 deletions docs/examples/yolov5m.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import numpy as np

from furiosa.models.vision import YOLOv5m
from furiosa.runtime import session
from furiosa.runtime.sync import create_runner

yolov5m = YOLOv5m.load()
yolov5m = YOLOv5m()

with session.create(yolov5m.enf) as sess:
with create_runner(yolov5m.model_source()) as runner:
image = cv2.imread("tests/assets/yolov5-test.jpg")
inputs, contexts = yolov5m.preprocess([image])
output = sess.run(np.expand_dims(inputs[0], axis=0)).numpy()
output = runner.run(np.expand_dims(inputs[0], axis=0))
yolov5m.postprocess(output, contexts=contexts)
2 changes: 1 addition & 1 deletion furiosa/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Furiosa Models"""
from . import errors, vision

__version__ = "0.9.0.dev0"
__version__ = "0.10.0.dev0"
__all__ = ["errors", "vision"]
161 changes: 161 additions & 0 deletions furiosa/models/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING, Collection, Optional, Tuple, Union

if TYPE_CHECKING:
from .types import Platform

import requests
import yaml

from . import errors

EXT_CALIB_YAML = "calib_range.yaml"
EXT_ONNX = "onnx"
DATA_DIRECTORY_BASE = Path(__file__).parent / "data"
CACHE_DIRECTORY_BASE = Path(
os.getenv(
"FURIOSA_MODELS_CACHE_HOME",
os.path.join(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache"), "furiosa/models"),
)
)
DVC_PUBLIC_HTTP_ENDPOINT = (
"https://furiosa-public-artifacts.s3-accelerate.amazonaws.com/furiosa-artifacts/"
)

module_logger = logging.getLogger(__name__)


def get_version_info() -> Optional[str]:
from furiosa.tools.compiler.api import VersionInfo

version_info = VersionInfo()
return f"{version_info.version}_{version_info.git_hash}"


def find_dvc_cache_directory(path: Path) -> Optional[Path]:
if path is None or path == path.parent:
return None
if (path / ".dvc").is_dir():
return path / ".dvc" / "cache"
return find_dvc_cache_directory(path.parent)


def parse_dvc_file(file_path: Path) -> Tuple[str, str, int]:
info_dict = yaml.safe_load(open(f"{file_path}.dvc").read())["outs"][0]
md5sum = info_dict["md5"]
return md5sum[:2], md5sum[2:], info_dict["size"]


def get_from_url(path: str, uri: Path, is_legacy_path: bool = False) -> bytes:
url = f"{DVC_PUBLIC_HTTP_ENDPOINT}{path}"
module_logger.debug(f"Fetching from remote: {url}")
with requests.get(url) as resp:
if resp.status_code != 200:
if not is_legacy_path:
# New dvc now stores data into /files/md5
return get_from_url(f"files/md5/{path}", uri, True)
raise errors.NotFoundInDVCRemote(uri, path)
data = resp.content
caching_path = CACHE_DIRECTORY_BASE / get_version_info() / (uri.name)
module_logger.debug(f"caching to {caching_path}")
caching_path.parent.mkdir(parents=True, exist_ok=True)
with open(caching_path, mode="wb") as f:
f.write(data)
return data


class ArtifactResolver:
def __init__(self, uri: Union[str, Path]):
self.uri = Path(uri)
# Note: DVC_REPO is to locate local DVC directory not remote git repository
self.dvc_cache_path = os.environ.get("DVC_REPO", find_dvc_cache_directory(Path.cwd()))
if self.dvc_cache_path is not None:
self.dvc_cache_path = Path(self.dvc_cache_path)
if self.dvc_cache_path.is_symlink():
self.dvc_cache_path = self.dvc_cache_path.readlink()
module_logger.debug(f"Found DVC cache directory: {self.dvc_cache_path}")

def _read(self, directory: str, filename: str) -> bytes:
# Try to find local cached file
local_cache_path = CACHE_DIRECTORY_BASE / get_version_info() / (self.uri.name)
if local_cache_path.exists():
module_logger.debug(f"Local cache exists: {local_cache_path}")
with open(local_cache_path, mode="rb") as f:
return f.read()

# Try to find real file along with DVC file (no DVC)
if Path(self.uri).exists():
module_logger.debug(f"Local file exists: {self.uri}")
with open(self.uri, mode="rb") as f:
return f.read()

module_logger.debug(f"{self.uri} not exists, resolving DVC")
if self.dvc_cache_path is not None:
cached: Path = self.dvc_cache_path / directory / filename
if cached.exists():
module_logger.debug(f"DVC cache hit: {cached}")
with open(cached, mode="rb") as f:
return f.read()
else:
module_logger.debug(f"DVC cache directory exists, but not having {self.uri}")

# Fetching from remote
return get_from_url(f"{directory}/{filename}", self.uri)

def read(self) -> bytes:
directory, filename, size = parse_dvc_file(self.uri)
data = self._read(directory, filename)
assert len(data) == size
return data


def resolve_artifact(src_name: str, full_path: Path) -> bytes:
try:
return ArtifactResolver(full_path).read()
except Exception as e:
raise errors.ArtifactNotFound(f"{src_name}:{full_path}") from e


def resolve_source(src_name: str, extension: str) -> bytes:
full_path = next((DATA_DIRECTORY_BASE / src_name).glob(f'*.{extension}.dvc'))
# Remove `.dvc` suffix
full_path = full_path.with_suffix('')
return resolve_artifact(src_name, full_path)


def resolve_model_source(src_name: str, num_pe: int = 2) -> bytes:
version_info = get_version_info()
if version_info is None:
raise errors.VersionInfoNotFound()
generated_path_base = DATA_DIRECTORY_BASE / f"generated/{version_info}"
if not generated_path_base.exists():
module_logger.warning("ENF does not exist. Trying to generate from source..")

try:
import onnx
import yaml

from furiosa.quantizer import ModelEditor, TensorType, get_pure_input_names, quantize
except ImportError:
raise errors.ExtraPackageRequired()
module_logger.warning(f"Returning quantized ONNX for {src_name}")
onnx_model = onnx.load_from_string(resolve_source(src_name, EXT_ONNX))
calib_range = yaml.full_load(resolve_source(src_name, EXT_CALIB_YAML))
editor = ModelEditor(onnx_model)
for input_name in get_pure_input_names(onnx_model):
editor.convert_input_type(input_name, TensorType.UINT8)
return quantize(onnx_model, calib_range)
file_name = f'{src_name}_warboy_{num_pe}pe.enf'
return resolve_artifact(src_name, generated_path_base / file_name)


def validate_postprocessor_type(
postprocessor_type: Union[str, "Platform"], postprocessor_map: Collection["Platform"]
):
if postprocessor_type not in postprocessor_map:
raise ValueError(
f"Not supported postprocessor type: {postprocessor_type}, "
f"Available choices: {', '.join(postprocessor_map)}"
)
Loading