Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7da825e
Support saving of pickleable Python objects
alahiff Jan 15, 2023
378c9f2
Add dependencies required for plots
alahiff Jan 16, 2023
6afade8
Remove accidental commit of testing line
alahiff Jan 16, 2023
694491e
Support matplotlib & plotly plots
alahiff Jan 16, 2023
83a9cc2
matplotlib not used
alahiff Jan 16, 2023
c2f70d7
Move function to utilities
alahiff Jan 16, 2023
6e60697
Add prepare_for_api function
alahiff Jan 16, 2023
b0dc689
Need to remove pickled object from json sent to API
alahiff Jan 16, 2023
c74ad3d
Correct assignment of run name when sending json to API
alahiff Jan 16, 2023
0eb73e9
Bug fixes to metrics & events
alahiff Jan 16, 2023
2c4c14b
Clarify error message
alahiff Jan 16, 2023
aa99ad5
Update
alahiff Jan 16, 2023
a9ea94f
Shorten line
alahiff Jan 16, 2023
95d01e4
Very minor adjustment
alahiff Jan 16, 2023
5c75f55
Filename was wrong
alahiff Jan 16, 2023
e1e7c06
Minor adjustment to message
alahiff Jan 16, 2023
229ac42
Update
alahiff Jan 16, 2023
21b9e78
Requests module not used
alahiff Jan 16, 2023
95c4081
dill was forgotten
alahiff Jan 16, 2023
36f8eb1
Tidied up serialization
alahiff Jan 17, 2023
9409e3c
Adjustments to serialization; add deserialization
alahiff Jan 17, 2023
bd0d0e0
Support deserialization in get_artifact
alahiff Jan 17, 2023
601f226
Remove modules not used
alahiff Jan 17, 2023
eaa5d37
Support optional pickling
alahiff Jan 17, 2023
a198cc6
Support optional pickling
alahiff Jan 17, 2023
2867153
Support pytorch tensors; adding tensorflow tensors
alahiff Jan 17, 2023
8ea88d4
Use numpy/pandas only when needed
alahiff Jan 17, 2023
bf9e9df
Remove partial tensorflow support
alahiff Jan 17, 2023
a21fcb1
Remove unused module import
alahiff Jan 17, 2023
6fec380
Bug fix - forgot to return the mimetype
alahiff Jan 17, 2023
31e95a9
Add serialization/deserialization tests for numpy & PyTorch arrays
alahiff Jan 17, 2023
ac21a1a
Add requirements.txt for tests
alahiff Jan 17, 2023
83de128
Use test-requirements.txt for tests
alahiff Jan 17, 2023
202403b
Add tests for numpy array & pytorch tensor mime types
alahiff Jan 17, 2023
a5a1fbf
Add mime-type tests for matplotlib & plotly
alahiff Jan 17, 2023
9386bbf
Add matplotlib dependency
alahiff Jan 17, 2023
d6fc48a
Add test for pickle serialization
alahiff Jan 17, 2023
4ca0de7
Add missing comments
alahiff Jan 17, 2023
e478e5c
Need to read first column as index
alahiff Jan 17, 2023
851ca7a
Add tests for Pandas dataframe mime-type and serialization/deserializ…
alahiff Jan 17, 2023
e1cfdf2
Support PyTorch state_dict serialization
alahiff Jan 17, 2023
e9cc491
Add simple PyTorch example
alahiff Jan 17, 2023
97bf378
Not everything is pickled
alahiff Jan 19, 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
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
python -m pip install --upgrade pip
pip install flake8 pytest
pip install -e .
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change log

## v0.8.0

* Support NumPy arrays, PyTorch tensors, Matplotlib and Plotly plots and picklable Python objects as artifacts.
* (Bug fix) Events in offline mode didn't work.

## v0.7.2

* Pydantic model is used for input validation.
Expand Down
158 changes: 158 additions & 0 deletions examples/PyTorch/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Taken from https://github.com/pytorch/examples/blob/main/mnist/main.py
from __future__ import print_function
import argparse
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.optim.lr_scheduler import StepLR
from simvue import Run


class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3, 1)
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.dropout1 = nn.Dropout(0.25)
self.dropout2 = nn.Dropout(0.5)
self.fc1 = nn.Linear(9216, 128)
self.fc2 = nn.Linear(128, 10)

def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)
x = self.dropout1(x)
x = torch.flatten(x, 1)
x = self.fc1(x)
x = F.relu(x)
x = self.dropout2(x)
x = self.fc2(x)
output = F.log_softmax(x, dim=1)
return output


def train(args, model, device, train_loader, optimizer, epoch, run):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % args.log_interval == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
run.log_metrics({"train.loss.%d" % epoch: float(loss.item())}, step=batch_idx)
if args.dry_run:
break


def test(model, device, test_loader, epoch, run):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability
correct += pred.eq(target.view_as(pred)).sum().item()

test_loss /= len(test_loader.dataset)
test_accuracy = 100. * correct / len(test_loader.dataset)

print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
test_accuracy))
run.log_metrics({'test.loss': test_loss,
'test.accuracy': test_accuracy}, step=epoch)


def main():
# Training settings
parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
parser.add_argument('--batch-size', type=int, default=64, metavar='N',
help='input batch size for training (default: 64)')
parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
help='input batch size for testing (default: 1000)')
parser.add_argument('--epochs', type=int, default=14, metavar='N',
help='number of epochs to train (default: 14)')
parser.add_argument('--lr', type=float, default=1.0, metavar='LR',
help='learning rate (default: 1.0)')
parser.add_argument('--gamma', type=float, default=0.7, metavar='M',
help='Learning rate step gamma (default: 0.7)')
parser.add_argument('--no-cuda', action='store_true', default=False,
help='disables CUDA training')
parser.add_argument('--no-mps', action='store_true', default=False,
help='disables macOS GPU training')
parser.add_argument('--dry-run', action='store_true', default=False,
help='quickly check a single pass')
parser.add_argument('--seed', type=int, default=1, metavar='S',
help='random seed (default: 1)')
parser.add_argument('--log-interval', type=int, default=10, metavar='N',
help='how many batches to wait before logging training status')
parser.add_argument('--save-model', action='store_true', default=False,
help='For Saving the current Model')
args = parser.parse_args()
use_cuda = not args.no_cuda and torch.cuda.is_available()
use_mps = not args.no_mps and torch.backends.mps.is_available()

torch.manual_seed(args.seed)

if use_cuda:
device = torch.device("cuda")
elif use_mps:
device = torch.device("mps")
else:
device = torch.device("cpu")

train_kwargs = {'batch_size': args.batch_size}
test_kwargs = {'batch_size': args.test_batch_size}
if use_cuda:
cuda_kwargs = {'num_workers': 1,
'pin_memory': True,
'shuffle': True}
train_kwargs.update(cuda_kwargs)
test_kwargs.update(cuda_kwargs)

transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
dataset1 = datasets.MNIST('../data', train=True, download=True,
transform=transform)
dataset2 = datasets.MNIST('../data', train=False,
transform=transform)
train_loader = torch.utils.data.DataLoader(dataset1,**train_kwargs)
test_loader = torch.utils.data.DataLoader(dataset2, **test_kwargs)

model = Net().to(device)
optimizer = optim.Adadelta(model.parameters(), lr=args.lr)

scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma)

run = Run()
run.init(tags=['PyTorch'])

for epoch in range(1, args.epochs + 1):
train(args, model, device, train_loader, optimizer, epoch, run)
test(model, device, test_loader, epoch, run)
scheduler.step()

if args.save_model:
run.save(model.state_dict(), "output", name="mnist_cnn.pt")

run.close()


if __name__ == '__main__':
main()

3 changes: 3 additions & 0 deletions examples/PyTorch/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
torch
torchvision
simvue
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
long_description_content_type="text/markdown",
url="https://simvue.io",
platforms=["any"],
install_requires=["requests", "msgpack", "tenacity", "pyjwt", "psutil", "pydantic"],
install_requires=["dill", "requests", "msgpack", "tenacity", "pyjwt", "psutil", "pydantic", "plotly"],
package_dir={'': '.'},
packages=["simvue"],
package_data={"": ["README.md"]},
Expand Down
2 changes: 1 addition & 1 deletion simvue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from simvue.client import Client
from simvue.handler import Handler
from simvue.models import RunInput
__version__ = '0.7.2'
__version__ = '0.8.0'
27 changes: 14 additions & 13 deletions simvue/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pickle
import requests

from .serialization import Deserializer
from .utilities import get_auth

CONCURRENT_DOWNLOADS = 10
Expand Down Expand Up @@ -51,7 +52,7 @@ def list_artifacts(self, run, category=None):

return None

def get_artifact(self, run, name):
def get_artifact(self, run, name, allow_pickle=False):
"""
Return the contents of the specified artifact
"""
Expand All @@ -62,23 +63,23 @@ def get_artifact(self, run, name):
except requests.exceptions.RequestException:
return None

if response.status_code == 200 and response.json():
url = response.json()[0]['url']

try:
response = requests.get(url, timeout=DOWNLOAD_TIMEOUT)
except requests.exceptions.RequestException:
return None
else:
if response.status_code != 200:
return None

url = response.json()[0]['url']
mimetype = response.json()[0]['type']

try:
content = pickle.loads(response.content)
except:
return response.content
else:
response = requests.get(url, timeout=DOWNLOAD_TIMEOUT)
except requests.exceptions.RequestException:
return None

content = Deserializer().deserialize(response.content, mimetype, allow_pickle)
if content is not None:
return content

return response.content

def get_artifact_as_file(self, run, name, path='./'):
"""
Download an artifact
Expand Down
11 changes: 9 additions & 2 deletions simvue/offline.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import codecs
import json
import logging
import os
import time
import uuid

from .utilities import get_offline_directory, create_file
from .utilities import get_offline_directory, create_file, prepare_for_api

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -90,9 +92,14 @@ def save_file(self, data):
"""
Save file
"""
if 'pickled' in data:
temp_file = f"{self._directory}/temp-{str(uuid.uuid4())}.pickle"
with open(temp_file, 'wb') as fh:
fh.write(data['pickled'])
data['pickledFile'] = temp_file
unique_id = time.time()
filename = f"{self._directory}/file-{unique_id}.json"
self._write_json(filename, data)
self._write_json(filename, prepare_for_api(data, False))
return True

def add_alert(self, data):
Expand Down
54 changes: 40 additions & 14 deletions simvue/remote.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import logging
import time
import requests

from .api import post, put
from .utilities import get_auth, get_expiry
from .utilities import get_auth, get_expiry, prepare_for_api

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,10 +52,13 @@ def create_run(self, data):

return self._name

def update(self, data):
def update(self, data, run=None):
"""
Update metadata, tags or status
"""
if run is not None:
data['name'] = run

try:
response = put(f"{self._url}/api/runs", self._headers, data)
except Exception as err:
Expand All @@ -69,10 +71,13 @@ def update(self, data):
self._error(f"Got status code {response.status_code} when updating run")
return False

def set_folder_details(self, data):
def set_folder_details(self, data, run=None):
"""
Set folder details
"""
if run is not None:
data['run'] = run

try:
response = put(f"{self._url}/api/folders", self._headers, data)
except Exception as err:
Expand All @@ -85,13 +90,16 @@ def set_folder_details(self, data):
self._error(f"Got status code {response.status_code} when updating folder details")
return False

def save_file(self, data):
def save_file(self, data, run=None):
"""
Save file
"""
if run is not None:
data['run'] = run

# Get presigned URL
try:
response = post(f"{self._url}/api/data", self._headers, data)
response = post(f"{self._url}/api/data", self._headers, prepare_for_api(data))
except Exception as err:
self._error(f"Got exception when preparing to upload file {data['name']} to object storage: {str(err)}")
return False
Expand All @@ -105,22 +113,40 @@ def save_file(self, data):

if 'url' in response.json():
url = response.json()['url']
try:
with open(data['originalPath'], 'rb') as fh:
response = put(url, {}, fh, is_json=False, timeout=UPLOAD_TIMEOUT)
if 'pickled' in data and 'pickledFile' not in data:
try:
response = put(url, {}, data['pickled'], is_json=False, timeout=UPLOAD_TIMEOUT)
if response.status_code != 200:
self._error(f"Got status code {response.status_code} when uploading file {data['name']} to object storage")
self._error(f"Got status code {response.status_code} when uploading object {data['name']} to object storage")
return None
except Exception as err:
self._error(f"Got exception when uploading file {data['name']} to object storage: {str(err)}")
return None
except Exception as err:
self._error(f"Got exception when uploading object {data['name']} to object storage: {str(err)}")
return None
else:
if 'pickledFile' in data:
use_filename = data['pickledFile']
else:
use_filename = data['originalPath']

try:
with open(use_filename, 'rb') as fh:
response = put(url, {}, fh, is_json=False, timeout=UPLOAD_TIMEOUT)
if response.status_code != 200:
self._error(f"Got status code {response.status_code} when uploading file {data['name']} to object storage")
return None
except Exception as err:
self._error(f"Got exception when uploading file {data['name']} to object storage: {str(err)}")
return None

return True

def add_alert(self, data):
def add_alert(self, data, run=None):
"""
Add an alert
"""
if run is not None:
data['run'] = run

try:
response = post(f"{self._url}/api/alerts", self._headers, data)
except Exception as err:
Expand Down
Loading