I am trying to use a huggingface dataset across di...
# ask-metaflow
b
I am trying to use a huggingface dataset across different steps in a metaflow run. however, it errors out. my guess is that subsequent steps after loading the dataset cant access the cache of the previous step. is it possible to save a hf dataset as a metaflow artifcat? I am running this on aws batch. sample code and error below:
Copy code
@step
    def load_data(self):
        from datasets import load_dataset

        with S3() as s3:
            s3obj = s3.get(self.org_data)
            dataset = load_dataset("csv",data_files=s3obj.path)
        self.dataset = dataset
        print(self.dataset)
        self.next(self.eda)

    @step
    def eda(self):
        print(self.dataset)
error:
Copy code
File "flow.py", line 133, in pre_nightingale_eda
    print(self.dataset)
  File "/flow/metaflow/metaflow/flowspec.py", line 224, in __getattr__
    x = self._datastore[name]
  File "/flow/metaflow/metaflow/datastore/task_datastore.py", line 45, in method
    return f(self, args, kwargs)
  File "/flow/metaflow/metaflow/datastore/task_datastore.py", line 836, in __getitem__
    _, obj = next(self.load_artifacts([name]))
  File "/flow/metaflow/metaflow/datastore/task_datastore.py", line 370, in load_artifacts
    yield name, pickle.loads(blob)
  File "/usr/local/lib/python3.8/site-packages/datasets/table.py", line 1069, in __setstate__
    table = _memory_mapped_arrow_table_from_file(path)
  File "/usr/local/lib/python3.8/site-packages/datasets/table.py", line 65, in _memory_mapped_arrow_table_from_file
    opened_stream = _memory_mapped_record_batch_reader_from_file(filename)
  File "/usr/local/lib/python3.8/site-packages/datasets/table.py", line 50, in _memory_mapped_record_batch_reader_from_file
    memory_mapped_stream = pa.memory_map(filename)
  File "pyarrow/io.pxi", line 1009, in pyarrow.lib.memory_map
  File "pyarrow/io.pxi", line 956, in pyarrow.lib.MemoryMappedFile._open
  File "pyarrow/error.pxi", line 144, in pyarrow.lib.pyarrow_internal_check_status
  File "pyarrow/error.pxi", line 113, in pyarrow.lib.check_status
FileNotFoundError: [Errno 2] Failed to open local file '/root/.cache/huggingface/datasets/csv/default-42f4d214a7c91375/0.0.0/eea64c71ca8b46dd3f537ed218fc9bf495d5707789152eb2764f5c78fa66d59d/csv-train-00000-of-00009.arrow'. Detail: [errno 2] No such file or directory
1
c
You are correct. it is possible to do this, but for a dataset any bigger than a few hundred MBs it usually makes more sense to use S3 as a cache. Here is a generic structure that might help: Flow • start • load_data ◦ download the data to the batch/k8s instance once, put it in a S3 "cache" accessible to all your Metaflow steps ◦ in the future if the data is already in the S3 cache there is no need to repeat this unless you observe changes in the data • train_model ◦ download data from the S3 cache to the batch/k8s instance ◦ the model itself can follow this same pattern if it is big, see this example's model store concept ◦ at the end you will likely want to push the trained model weights to a similar S3 cache for models • end
This pattern will also help you store pointers to these data and model locations in your flows, making it much easier to link flows together for e.g., resuming from the training of a previous run's checkpoint in the next flow run.
b
by a s3 cache you just mean to save the hf dataset to s3 and load it from there whenever needed in future steps?
c
ya. it requires ~5 more lines of code (s3.put in one step, s3.get in the downstream ones) but is typically much faster than trying to serialize the dataset to pass it between steps
it also naturally extends to entire directories
if you'd like to share some code samples we can work on a few examples to automate it so it is as easy as
self.
- the main point is that the serialization gets kinda slow for things as big as training datasets
b
ok. thanks. let me write up some sample code.
👍 1
Copy code
@card(id="eda", type="blank")
    @step
    def load_data(self):

        from datasets import load_dataset

        def walk_directory(root):
            path_keys = []
            for path, subdirs, files in os.walk(root):
                for name in files:
                # create a tuple of (key, path)
                    path_keys.append((os.path.relpath(os.path.join(path, name), root),os.path.join(path, name)))
            return path_keys

        logger.info(f'loading data')


        with S3() as s3:
            s3obj = s3.get(self.org_data)
            dataset = load_dataset("csv")
            print(dataset.shape)

        #saving dataset to s3
        os.mkdir('/flow/hfdataset')
        dataset.save_to_disk('/flow/hfdataset')
        with S3(s3root='<s3://bucket/hfdataset>') as s3:
            s3.put_files(walk_directory('/flow/hfdataset'))

        batch_size = 60
        num_batches = math.ceil(num_rows / batch_size)
        self.batches = [(i*batch_size, min((i+1)*batch_size, num_rows)) for i in range(num_batches)]

        self.next(self.hit_nightingale_model,foreach='batches')

    @step
    def hit_model(self):
        from datasets import load_from_disk
        import shutil

        def download_model(s3_path, download_path='/root/dir/'):
            final_path = s3_path
            os.makedirs('/root/dir', exist_ok=True)
            with S3(s3root=final_path) as s3:
                for s3obj in s3.get_all():
                    print('s3 key --->', s3obj.key)
                    move_path = os.path.join(download_path, s3obj.key)
                    print('move path --->', move_path)
                    if not os.path.exists(os.path.dirname(move_path)):
                        os.makedirs(os.path.dirname(move_path), exist_ok=True)
                    shutil.move(s3obj.path, os.path.join(download_path, s3obj.key))
                #print('dirpath ---->', (os.path.join(download_path, s3obj.key)))
                dataset=load_from_disk('/root/dir/')
                print(dataset)
                return dataset

        print('self.input --->', self.input)
        indices = self.input

        model = Model()
        dataset = download_model('<s3://bucket/hfdataset>')
        print(dataset)
        dataset = dataset['train'].select(range(indices[0],indices[1]))
        print(dataset)

        def process_row(row):
            result = model.run(report)
            return result

        self.result_dataset =  dataset.map(process_row, num_proc=32,batch_size=50000)
        self.next(self.join_results)

    @step
    def join_results(self,inputs):
        for  dataset in result_datasets:
            print(datset)
            print(dataset['train'][0])
this is similar to what I am trying to do
one thing i nonticed while implementing this: i have to write the hf dataset to disk before I can upload it to s3 which increases the memory requirements. I guess I can use s3fs but I have heard that its slow.
c
@batch also supports a tmpfs argument, which can obviate some need to increase disk space by letting you keep the data in volatile storage in whatever task pushes it to s3
we could pair this with the HF dataset streaming mode to keep requirements on the compute decorator low
b
ok. but, this the upload/download paradigm you would recommend to work with hf datasets?
c
that is the general pattern I would suggest for big datasets. if you only want to use the smaller datasets in hf hub, then you can just redownload them in each task
b
how can we save smaller ones as artifacts? re:
You are correct. it is possible to do this, but for a dataset any bigger than a few hundred MBs
c
Can you load the arrow tables or csvs as pandas dataframes and serialize them?
b
pandas df ends up using a lot of memory thats why we were trying to move from pandas to hf dataset.
c
i see. this means you want to memory-map via arrow to the data on disk. somehow the data needs to get to a place where arrow can memory-map to in each task for this to work. you can either download from huggingface api in each task or do it using s3 (which will probably be faster if that matters - hf has api for this or you can use metaflow s3). that is why serializing only the hf dataset object isn't working, since the second task that uses the serialized thing isn't calling the huggingface api that actually moves the data to where arrow can memory map to it.
b
ok. thanks,
Hey Eddie, just a follow up question- whats the advantage of using tmpfs? does it also help in speeding up the upload/download of data to s3?
c
Hey! tmpfs reduces memory requirement, it helps when you scale up data throughput. See this blog
b
thank you!
🤗 1