Zarr - scalable storage of tensor Zarr - scalable storage of tensor - - PowerPoint PPT Presentation

zarr scalable storage of tensor zarr scalable storage of
SMART_READER_LITE
LIVE PREVIEW

Zarr - scalable storage of tensor Zarr - scalable storage of tensor - - PowerPoint PPT Presentation

Zarr - scalable storage of tensor Zarr - scalable storage of tensor data for parallel and distributed data for parallel and distributed computing computing Alistair Miles ( @alimanfoo ) - SciPy 2019 These slides:


slide-1
SLIDE 1

Zarr - scalable storage of tensor Zarr - scalable storage of tensor data for parallel and distributed data for parallel and distributed computing computing

Alistair Miles ( ) - SciPy 2019

These slides:

@alimanfoo

https://zarr-developers.github.io/slides/scipy-2019.html

slide-2
SLIDE 2
slide-3
SLIDE 3

Motivation: Why Zarr? Motivation: Why Zarr?

slide-4
SLIDE 4

Problem statement Problem statement

There is some computation we want to perform. Inputs and outputs are multidimensional arrays (a.k.a. tensors). 5 key features...

slide-5
SLIDE 5

(1) Larger than memory (1) Larger than memory

Input and/or output tensors are too big to fit comfortably in main memory.

slide-6
SLIDE 6

(2) Computation can be parallelised (2) Computation can be parallelised

At least some part of the computation can be parallelised by processing data in chunks.

slide-7
SLIDE 7

E.g., embarassingly parallel E.g., embarassingly parallel

slide-8
SLIDE 8

(3) I/O is the bottleneck (3) I/O is the bottleneck

Computational complexity is moderate → significant amount

  • f time is spent in reading and/or writing data.

N.B., bottleneck may be due to (a) limited I/O bandwidth, (b) I/O is not parallel.

slide-9
SLIDE 9

(4) Data are compressible (4) Data are compressible

Compression is a very active area of innovation. Modern compressors achieve good compression ratios with very high speed. Compression can increase effective I/O bandwidth, sometimes dramatically.

slide-10
SLIDE 10

(5) Speed matters (5) Speed matters

Rich datasets → exploratory science → interactive analysis → many rounds of summarise, visualise, hypothesise, model, test, repeat. E.g., genome sequencing.

Now feasible to sequence genomes from 100,000s of individuals and compare them. Each genome is a complete molecular blueprint for an organism → can investigate many different molecular pathways and processes. Each genome is a history book handed down through the ages, with each generation making its mark → can look back in time and infer major demographic and evolutionary events in the history of populations and species.

slide-11
SLIDE 11

Problem: key features Problem: key features

  • 0. Inputs and outputs are

tensors.

  • 1. Data are larger than memory.
  • 2. Computation can be

parallelised.

  • 3. I/O is the bottleneck.
  • 4. Data are compressible.
  • 5. Speed matters.
slide-12
SLIDE 12

Solution Solution

  • 1. Chunked, parallel tensor computing

framework.

  • 2. Chunked, parallel tensor storage library.

Align the chunks!

slide-13
SLIDE 13

Parallel computing framework for chunked tensors. Write code using a numpy-like API. Parallel execution on local workstation, HPC cluster, Kubernetes cluster, ...

import dask.array as da a = ... # what goes here? x = da.from_array(a) y = (x - x.mean(axis=1)) / x.std(axis=1) u, s, v = da.linalg.svd_compressed(y, 20) u = u.compute()

slide-14
SLIDE 14

Scale up ocean / atmosphere / land / climate science. Aim to handle petabyte-scale datasets on HPC and cloud platforms. Using Dask. Needed a tensor storage solution. Interested to use cloud object stores: Amazon S3, Azure Blob Storage, Google Cloud Storage, ...

slide-15
SLIDE 15

Tensor storage: prior art Tensor storage: prior art

slide-16
SLIDE 16

HDF5 (h5py) HDF5 (h5py)

Store tensors ("datasets"). Divide data into regular chunks. Chunks are compressed. Group tensors into a hierarchy. Smooth integration with NumPy...

import h5py x = h5py.File('example.h5')['x'] # read 1000 rows into numpy array y = x[:1000]

slide-17
SLIDE 17

HDF5 - limitations HDF5 - limitations

No thread-based parallelism. Cannot do parallel writes with compression. Not easy to plug in a new compressor. No support for cloud object stores (but see ). See also by Cyrille Rossant. Kita moving away from HDF5

slide-18
SLIDE 18

bcolz bcolz

Developed by . Chunked storage, primarily intended for storing 1D arrays (table columns), but can also store tensors. Implementation is simple (in a good way). Data format on disk is simple - one file for metadata, one file for each chunk. Showcase for the . Francesc Alted Blosc compressor

slide-19
SLIDE 19

bcolz - limitations bcolz - limitations

Chunking in 1 dimension only. No support for cloud object stores.

slide-20
SLIDE 20

How hard could it be ... How hard could it be ...

... to implement a chunked storage library for tensor data that supported parallel reads, parallel writes, was easy to plug in new compressors, and easy to plug in different storage systems like cloud object stores?

slide-21
SLIDE 21

<montage/> <montage/>

3 years, 1,107 commits, 39 releases, 259 issues, 165 PRs, and at least 2 babies later ...

slide-22
SLIDE 22

Zarr Python Zarr Python

$ pip install zarr $ conda install -c conda-forge zarr >>> import zarr >>> zarr.__version__ '2.3.2'

slide-23
SLIDE 23

Conceptual model based on HDF5 Conceptual model based on HDF5

Multiple arrays (a.k.a. datasets) can be created and organised into a hierarchy of groups. Each array is divided into regular shaped chunks. Each chunk is compressed before storage.

slide-24
SLIDE 24

Creating a hierarchy Creating a hierarchy

Using DirectoryStore the data will be stored in a directory on the local file system.

>>> store = zarr.DirectoryStore('example.zarr') >>> root = zarr.group(store) >>> root <zarr.hierarchy.Group '/'>

slide-25
SLIDE 25

Creating an array Creating an array

Creates a 2-dimensional array of 32-bit integers with 10,000 rows and 10,000 columns. Divided into chunks where each chunk has 1,000 rows and 1,000 columns. There will be 100 chunks in total, arranged in a 10x10 grid.

>>> hello = root.zeros('hello', ... shape=(10000, 10000), ... chunks=(1000, 1000), ... dtype='<i4') >>> hello <zarr.core.Array '/hello' (10000, 10000) int32>

slide-26
SLIDE 26

Creating an array (h5py-style API) Creating an array (h5py-style API)

>>> hello = root.create_dataset('hello', ... shape=(10000, 10000), ... chunks=(1000, 1000), ... dtype='<i4') >>> hello <zarr.core.Array '/hello' (10000, 10000) int32>

slide-27
SLIDE 27

Creating an array (big) Creating an array (big)

>>> big = root.zeros('big', ... shape=(100_000_000, 100_000_000), ... chunks=(10_000, 10_000), ... dtype='i4') >>> big <zarr.core.Array '/big' (100000000, 100000000) int32>

slide-28
SLIDE 28

Creating an array (big) Creating an array (big)

That's a 35 petabyte array. N.B., chunks are initialized on write.

>>> big.info Name : /big Type : zarr.core.Array Data type : int32 Shape : (100000000, 100000000) Chunk shape : (10000, 10000) Order : C Read-only : False Compressor : Blosc(cname='lz4', clevel=5, shuffle=SHUFFLE, bl Store type : zarr.storage.DirectoryStore

  • No. bytes : 40000000000000000 (35.5P)
  • No. bytes stored : 355

Storage ratio : 112676056338028.2 Chunks initialized : 0/100000000

slide-29
SLIDE 29

Writing data into an array Writing data into an array

Same API as writing into numpy array or h5py dataset.

>>> big[0, 0:20000] = np.arange(20000) >>> big[0:20000, 0] = np.arange(20000)

slide-30
SLIDE 30

Reading data from an array Reading data from an array

Same API as slicing a numpy array or reading from an h5py dataset.

>>> big[0:1000, 0:1000] array([[ 0, 1, 2, ..., 997, 998, 999], [ 1, 0, 0, ..., 0, 0, 0], [ 2, 0, 0, ..., 0, 0, 0], ..., [997, 0, 0, ..., 0, 0, 0], [998, 0, 0, ..., 0, 0, 0], [999, 0, 0, ..., 0, 0, 0]], dtype=int32)

slide-31
SLIDE 31

Chunks are initialized on write Chunks are initialized on write

>>> big.info Name : /big Type : zarr.core.Array Data type : int32 Shape : (100000000, 100000000) Chunk shape : (10000, 10000) Order : C Read-only : False Compressor : Blosc(cname='lz4', clevel=5, shuffle=SHUFFLE, bl Store type : zarr.storage.DirectoryStore

  • No. bytes : 40000000000000000 (35.5P)
  • No. bytes stored : 5171386 (4.9M)

Storage ratio : 7734870303.6 Chunks initialized : 3/100000000

slide-32
SLIDE 32

Files on disk Files on disk

$ tree -a example.zarr example.zarr ├── big │ ├── 0.0 │ ├── 0.1 │ ├── 1.0 │ └── .zarray ├── hello │ └── .zarray └── .zgroup 2 directories, 6 files

slide-33
SLIDE 33

Array metadata Array metadata

$ cat example.zarr/big/.zarray { "chunks": [ 10000, 10000 ], "compressor": { "blocksize": 0, "clevel": 5, "cname": "lz4", "id": "blosc", "shuffle": 1 }, "dtype": "<i4", "fill_value": 0, "filters": null, "order": "C", "shape": [ 100000000, 100000000 ], "zarr_format": 2 }

slide-34
SLIDE 34

Reading unwritten regions Reading unwritten regions

No data on disk, fill value is used (in this case zero).

>>> big[-1000:, -1000:] array([[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], dtype=int32)

slide-35
SLIDE 35

Reading the whole array Reading the whole array

Read the whole array into memory (if you can!)

>>> big[:] MemoryError

slide-36
SLIDE 36

zarr.DirectoryStore, zarr.ZipStore, zarr.DBMStore, zarr.LMDBStore, zarr.SQLiteStore, zarr.MongoDBStore, zarr.RedisStore, zarr.ABSStore, s3fs.S3Map, gcsfs.GCSMap, ...

Pluggable storage Pluggable storage

slide-37
SLIDE 37

DirectoryStore DirectoryStore

>>> store = zarr.DirectoryStore('example.zarr') >>> root = zarr.group(store) >>> big = root['big'] >>> big <zarr.core.Array '/big' (100000000, 100000000) int32>

slide-38
SLIDE 38

DirectoryStore (reminder) DirectoryStore (reminder)

$ tree -a example.zarr example.zarr ├── big │ ├── 0.0 │ ├── 0.1 │ ├── 1.0 │ └── .zarray ├── hello │ └── .zarray └── .zgroup 2 directories, 6 files

slide-39
SLIDE 39

ZipStore ZipStore

$ cd example.zarr && zip -r0 ../example.zip ./* >>> store = zarr.ZipStore('example.zip') >>> root = zarr.group(store) >>> big = root['big'] >>> big <zarr.core.Array '/big' (100000000, 100000000) int32>

slide-40
SLIDE 40

Google cloud storage (via Google cloud storage (via ) gcsfs gcsfs

$ gsutil config $ gsutil rsync -ru example.zarr/ gs://zarr-demo/example.zarr/ >>> import gcsfs >>> gcs = gcsfs.GCSFileSystem(token='anon', access='read_only') >>> store = gcsfs.GCSMap('zarr-demo/example.zarr', gcs=gcs, check=Fal >>> root = zarr.group(store) >>> big = root['big'] >>> big <zarr.core.Array '/big' (100000000, 100000000) int32>

slide-41
SLIDE 41

Google cloud storage Google cloud storage

slide-42
SLIDE 42
slide-43
SLIDE 43

Store interface Store interface

Any storage system can be used with Zarr if it can provide a key/value interface. Keys are strings, values are bytes. In Python, we use the MutableMapping interface. __getitem__ __setitem__ __iter__ I.e., anything dict-like can be used as a Zarr store.

slide-44
SLIDE 44

E.g., ZipStore implementation E.g., ZipStore implementation

( is slightly more complicated, but this is the essence.)

class ZipStore(MutableMapping): def __init__(self, path, ...): self.zf = zipfile.ZipFile(path, ...) def __getitem__(self, key): with self.zf.open(key) as f: return f.read() def __setitem__(self, key, value): self.zf.writestr(key, value) def __iter__(self): for key in self.zf.namelist(): yield key

Actual implementation

slide-45
SLIDE 45

Parallel computing with Zarr Parallel computing with Zarr

A Zarr array can have multiple concurrent readers*. A Zarr array can have multiple concurrent writers*. Both multi-thread and multi-process parallelism are supported. GIL is released during critical sections (compression and decompression).

* Depending on the store.

slide-46
SLIDE 46

Dask + Zarr Dask + Zarr

See docs for , , , .

import dask.array as da import zarr # set up input store = ... # some Zarr store root = zarr.group(store) big = root['big'] big = da.from_array(big) # define computation

  • utput = big * 42 + ...

# if output is small, compute to memory

  • = output.compute()

# if output is big, compute and write directly to Zarr da.to_zarr(output, store, component='output')

da.from_array() da.from_zarr() da.to_zarr() da.store()

slide-47
SLIDE 47

Write locks? Write locks?

If each writer is writing to a different region of an array, and all writes are aligned with chunk boundaries, then locking is not required.

slide-48
SLIDE 48

Write locks? Write locks?

If each writer is writing to a different region of an array, and writes are not aligned with chunk boundaries, then locking is required to avoid contention and/or data loss.

slide-49
SLIDE 49

Write locks? Write locks?

Zarr does support chunk-level write locks for either multi- thread or multi-process writes. But generally easier and better to align writes with chunk boundaries where possible. See Zarr tutorial for . further info on synchronisation

slide-50
SLIDE 50

Pluggable compressors Pluggable compressors

slide-51
SLIDE 51

Compressor benchmark (genomic data) Compressor benchmark (genomic data)

http://alimanfoo.github.io/2016/09/21/genotype-compression-benchmark.html

slide-52
SLIDE 52

Available compressors (via Available compressors (via )

Blosc, Zstandard, LZ4, Zlib, BZ2, LZMA, ...

numcodecs numcodecs

import zarr from numcodecs import Blosc store = zarr.DirectoryStore('example.zarr') root = zarr.group(store) compressor = Blosc(cname='zstd', clevel=1, shuffle=Blosc.BITSHUFFLE) big2 = root.zeros('big2', shape=(100_000_000, 100_000_000), chunks=(10_000, 10_000), dtype='i4', compressor=compressor)

slide-53
SLIDE 53

Compressor interface Compressor interface

The numcodecs defines the interface for filters and compressors for use with Zarr. Built around the . Codec API Python buffer protocol

slide-54
SLIDE 54

class Zlib(Codec): def __init__(self, level=1): self.level = level def encode(self, buf): # normalise inputs buf = ensure_contiguous_ndarray(buf) # do compression return zlib.compress(buf, self.level) def decode(self, buf, out=None): # normalise inputs buf = ensure_contiguous_ndarray(buf) if out is not None:

  • ut = ensure_contiguous_ndarray(out)

# do decompression dec = zlib.decompress(buf) return ndarray_copy(dec, out)

slide-55
SLIDE 55

Zarr specification Zarr specification

slide-56
SLIDE 56

Other Zarr implementations Other Zarr implementations

  • C++ implementation using xtensor
  • native Julia implementation
  • Scala implementation

WIP: z5 Zarr.jl ndarray.scala NetCDF and native cloud storage access via Zarr

slide-57
SLIDE 57

Integrations and applications Integrations and applications

slide-58
SLIDE 58

Xarray, Intake, Pangeo Xarray, Intake, Pangeo

, . for data catalogs has plugin with Zarr support. Used by Pangeo for their ...

(Here's the .)

xarray.open_zarr() xarray.Dataset.to_zarr() Intake project intake-xarray cloud datastore

import intake cat_url = 'https://raw.githubusercontent.com/pangeo-data/pangeo-data cat = intake.Catalog(cat_url) ds = cat.atmosphere.gmet_v1.to_dask()

underlying data catalog entry

slide-59
SLIDE 59

https://medium.com/informatics-lab/creating-a-data-format-for-high-momentum-datasets- a394fa48b671

slide-60
SLIDE 60

Microscopy (OME) Microscopy (OME)

See . OME's position regarding file formats

slide-61
SLIDE 61

Single cell biology Single cell biology

using Zarr with and to scale single cell gene expression analyses. The data portal uses Zarr for . Use Zarr for image-based transcriptomics ( )? Work by Laserson lab ScanPy AnnData Human Cell Atlas storage of gene expression matrices starfish

slide-62
SLIDE 62

Future Future

Zarr/ convergence. . N5 Zarr protocol spec v3 Community!

slide-63
SLIDE 63

Credits Credits

. Everyone who has contributed code or raised or commented on an issue or PR, thank you! UK MRC and Wellcome Trust for supporting @alimanfoo. Zarr is a community-maintained open source project - please think of it as yours! Zarr core development team