When I did this with my custom data loader, for so...
# hamilton-help
i
When I did this with my custom data loader, for some reason the loader got called with the entire list of inputs instead of one at a time. How can I debug this?
t
what node did you include in the
.execute()
call? If you request nodes between
Parallelizable
and
Collect
, it will try to eagerly collect values into a list instead of "one at a time" in each branch
🤔 1
i
Something that's way downstream of that... Unfortunately I cannot share my code publicly, and it'll take me some time to come up with a MCVE
t
Let me know if that summarizes: •
inputs
is a list of file paths • from
code
to
reduce_df
you load multiple datasets using a custom
DataLoader
(let's say its many partition of
.parquet
) • the
Collect
node merges the partitioned dataset • you do downstream computation If I understand, your
DataLoader
works, but once you added
Parallelizable/Collect
, it no longer worked?
s
@Iliya R could be a 🐛 . The data loader should only get one value.
👍 1
@Iliya R can’t seem to recreate it.
Copy code
import pandas as pd
from hamilton.function_modifiers import load_from, source
from hamilton.htypes import Parallelizable, Collect

def code(inputs: list) -> Parallelizable[str]:
   for _code in inputs:
      yield _code

@load_from.csv(path=source("code"))
def data_set(df: pd.DataFrame) -> pd.DataFrame:
   # some processing
   return df

def reduced_df(data_set: Collect[pd.DataFrame]) -> pd.DataFrame:
   return pd.concat(data_set)

def rest_of_my_computation(reduced_df: pd.DataFrame) -> object:
    # do something
    return reduced_df
this works for me:
Copy code
from hamilton import driver
dr = driver.Builder().with_modules(example).enable_dynamic_execution(allow_experimental_mode=True).build()
dr.execute(["rest_of_my_computation"], inputs={"inputs": ["a", "b", "c"]})
i
Roughly what I have:
Copy code
# data_loading.py

def symbol_generator(symbols: Iterable[str]) -> Parallelizable[str]:
    yield from symbols


@dataclass(frozen=True)
class CustomLoader(DataLoader):
    """Loads data using the API."""
    symbol: str
    # other params

    def load_data(self, _: type[type]) -> tuple[pd.DataFrame, dict[str, Any]]:
        raw_df: pd.DataFrame = (
           ...
        )
        return raw_df, get_dataframe_metadata(raw_df)

    @classmethod
    def applicable_types(cls) -> Collection[type]:
        return [pd.DataFrame]

    @classmethod
    def name(cls) -> str:
        return "mdq"
Copy code
# data_type_q.py

@load_from.mdq(
    symbol=source("symbol"),
    # other parameters
)
def data_type_q(input_data: pd.DataFrame) -> pd.DataFrame:
    return input_data

@subdag(
    data_loading,  # this is where `symbol_generator` is defined
    inputs={
        "symbol": source("symbol_generator"),
         # other params
    },
)
def tagged_q(data_type_q: pd.DataFrame, symbol: str) -> pd.DataFrame:
    return data_type_q.assign(symbol=symbol)


def mdq_all_symbols(tagged_q: Collect[pd.DataFrame]) -> pd.DataFrame:
    mdq_df: pd.DataFrame = pd.concat(tagged_q, axis=0, sort=True, ignore_index=True)
    return mdq_df.set_index(["timestamp", "symbol"]).sort_index()
So my custom loader class gets initialized with the full list of values, then in
load_data
I get multiple symbols.
Yeah, it does work with a single symbol
BTW the reason I'm using a
@subdag
is I need the list of symbols for multiple loading processes, and so I need to consume it more than once
e
SO yeah, I think this has to do with the subdag interaction — there’s something odd here going on
i
Based on looking or based on local testing?
e
Trying to run locally but I can’t reproduce/get it to work yet
That said, taking a step back, you’re running the parallel generator inside a subdag, right?
👍 1
OK, doctored it up, and this works. Not 100% sure about the use of subdag — using it with parallelizable is often a bit redundant. Note that the biggest difference is that I just created
symbol
as a function. Due to limitations (that I think you mentioned on github)
symbol_generator
is not allowed to be consumed twice. So a dummy (as I think you suggested above/hinted at) is actually a nice way to do it
Copy code
# data_loading.py
from dataclasses import dataclass
from typing import Iterable, Collection, Any

import pandas as pd

from hamilton.function_modifiers import source, load_from, subdag
from hamilton.htypes import Parallelizable, Collect
from hamilton.io.data_adapters import DataLoader


def symbol_generator(symbols: Iterable[str]) -> Parallelizable[str]:
    yield from symbols


@dataclass(frozen=True)
class CustomLoader(DataLoader):
    """Loads data using the API."""
    symbol: str

    # other params

    def load_data(self, _: type[type]) -> tuple[pd.DataFrame, dict[str, Any]]:
        raw_df: pd.DataFrame = pd.DataFrame({"timestamp": [1, 2, 3], "value": [1, 2, 3]})

        return raw_df, {}

    @classmethod
    def applicable_types(cls) -> Collection[type]:
        return [pd.DataFrame]

    @classmethod
    def name(cls) -> str:
        return "mdq"


from hamilton.registry import register_adapter

register_adapter(CustomLoader)


def symbol(symbol_generator: str) -> str:
    return symbol_generator


@load_from.mdq(
    symbol=source("symbol"),
    # other parameters
)
def data_type_q(input_data: pd.DataFrame) -> pd.DataFrame:
    return input_data

def tagged_q(data_type_q: pd.DataFrame, symbol: str) -> pd.DataFrame:
    return data_type_q.assign(symbol=symbol)


def mdq_all_symbols(tagged_q: Collect[pd.DataFrame]) -> pd.DataFrame:
    mdq_df: pd.DataFrame = pd.concat(tagged_q, axis=0, sort=True, ignore_index=True)
    return mdq_df.set_index(["timestamp", "symbol"]).sort_index()


if __name__ == "__main__":
    import __main__
    from hamilton import driver

    dr = driver.Builder().with_modules(__main__).enable_dynamic_execution(allow_experimental_mode=True).build()
    dr.display_all_functions("./output.png")
    dr.execute(["mdq_all_symbols"], inputs={"symbols": ["a", "b", "c"]})
output.png
s
@Elijah Ben Izzy I think @Iliya R fixed his issue? or?
i
I did not fix the generator not being emitted one item at a time
Which brings me to the point of nested parallelization... If it's impossible, maybe it's good that it didn't work for me here... I can use it where it matters more
@Elijah Ben Izzy please clarify - did you change something to make it work, or it just worked for you?
e
So I made two changes: (1) the one you mentioned (referring to the generator inside the subdag) (2) Adding a dummy variable —
symbol
now accepts/passes through
symbol_generator
. All the others refer to
symbol
.This made it clearer for me but I realized it’s not a documented bug so it might not be necessary. I just like it as it means there’s one parallel edge in and one collect out.
i
Great, thanks! I hope I'll get a collector node marker in my DAG as well now...
e
Yeah!
i
I tried to apply the above to my system but it still did not make the iterator yield symbols one at a time, nor did I get a red Collect node in my DAG, which seems to be a consistent symptom. Perhaps I oversimplified my problem when presenting it. I guess I'll see you at the office hours.
e
Hmm, happy to help at office hours (although might be @Stefan Krawczyk, I have an appointment then), or schedule another time. Curious what went wrong — if you can come up with a repro (without sensitive information of course) then happy to debug
i
Ok, I'll try to do that
Do you have a scheduling tool like the one Stefan does (that shows when you're available)?
e
Yep, although it’s not always 100% synced: https://calendly.com/elijahbenizzy. I’ll reschedule if a time doesn’t work so feel free to take some time
👍 1
i
Scheduling calls with the west coast is harder than I thought 😅
😆 1