gokart

repository·master·Indexed 43 days ago

https://github.com/m3dev/gokart

A Python framework and wrapper for Luigi designed for building reproducible Machine Learning pipelines. It provides automatic parameter-based reruns, type-safe task dependencies via TaskOnKart generics, and support for cloud storage (S3/GCS). Key features include metadata persistence, pandas.DataFrame type and column checking, automatic seeding for numpy and random, and concurrency control via Redis.

Tokens
29K
Snippets
106
Records
128
Agent score
56%

What's inside gokart

  1. What is gokart?

    master

    gokart is a wrapper for the luigi data pipeline library designed specifically for Machine Learning pipelines. It focuses on solving reproducibility, managing task dependencies, enforcing good code constraints, and providing ease of use for creating ML batches.

    Key features include:

    • Reproducibility: Automatically stores task output data, module versions, processing time, random seeds, logs, and task parameters in separate .pkl files indexed by hash.
    • Automatic Reruns: If a task parameter changes, the hash changes, triggering spontaneous reruns of the task and all its dependents.
    • Memory Efficiency: Intermediate data is exchanged via files (supporting GCS or S3) rather than keeping everything in memory.
    • Data Integrity: Provides pandas.DataFrame type and column checking during I/O.
    • Concurrency Control: Tasks are locked via Redis to prevent conflicts during parallel execution.
    • Automatic Seeding: Automatically fixes seeds for numpy and random.
  2. Key features of gokart for ML pipelines

    master

    Gokart is designed for reproducible Machine Learning pipelines with the following capabilities:

    • Metadata Persistence: For each task, a .pkl file stores the output data, module versions, processing time, random seeds, logs, and class variable parameters.
    • Automatic Reruns: The pipeline automatically reruns tasks if their parameters change.
    • Cloud Storage: Supports S3 and GCS for intermediate task results.
    • Memory Efficiency: Intermediate results are exchanged as files rather than kept in memory.
    • Data Integrity: Provides pandas.DataFrame type and column checking during I/O.
    • Reproducibility: Automatically fixes seeds for numpy and random.
    • Concurrency: Tasks are locked via Redis to support parallel execution safely.
  3. Log custom information in TaskOnKart

    master

    You can attach arbitrary metadata to a task that will be visible in task information reports (like CLI --tree-info-mode=all or the DataFrame table) by using the self.task_log dictionary within the run() method.

    Example:

    class MyTask(gokart.TaskOnKart):
        def run(self):
            # Any dictionary entry added here will be captured in task logs
            self.task_log['my_custom_key'] = 'my_custom_value'
    import gokart
    
    class SampleTaskLog(gokart.TaskOnKart):
        def run(self):
            # Add some logs.
            self.task_log['sample key'] = 'sample value'
    
    if __name__ == '__main__':
        SampleTaskLog().run()
        gokart.run([
            '--tree-info-mode=all',
            '--tree-info-output-path=sample_task_log.txt',
            'SampleTaskLog',
            '--local-scheduler'])
  4. Handle empty DataFrames with fail_on_empty_dump

    master

    By default, if a task produces an empty pandas.DataFrame, gokart will complete the task and output the empty DataFrame. However, empty DataFrames can sometimes hide bugs or incorrect data sources.

    To catch these issues early, you can set the fail_on_empty_dump attribute to True. When this is enabled, calling self.dump(df) with an empty DataFrame will raise a gokart.errors.EmptyDumpError.

    You can enable this via the CLI using the --fail-on-empty-dump flag.

    ```python
    import gokart
    import pandas as pd
    
    class EmptyTask(gokart.TaskOnKart):
        def run(self):
            df = pd.DataFrame()
            # If fail_on_empty_dump is True, this raises EmptyDumpError
            self.dump(df)

    CLI usage:

    $ python main.py EmptyTask --fail-on-empty-dump true

    Output: EmptyDumpError

  5. Type checking TaskOnKart parameters and generics

    master

    The Mypy plugin provides two main type-safety benefits for gokart tasks:

    1. Parameter Validation: It ensures that when instantiating a task, the arguments passed match the annotated types of the luigi.Parameter or luigi.IntParameter fields. Note: All parameters must be annotated for this to work.
    2. Generic Type Inference: It checks gokart.TaskOnKart[T] generic types. When you use self.load(task_instance_parameter), Mypy correctly infers the return type based on the generic type of the parameter (e.g., TaskOnKart[int] results in an int being returned by .load()). It also validates that the tasks passed to TaskInstanceParameter match the expected generic type.
    import gokart
    import luigi
    
    class Foo(gokart.TaskOnKart):
        # All parameters must be annotated
        foo: int = luigi.IntParameter(default=1)
        bar: str = luigi.Parameter()
    
    Foo(foo=1, bar='2')   # OK
    Foo(foo='1')          # Mypy error: foo is not int and bar is missing
    
    class SampleTask(gokart.TaskOnKart):
        str_task: gokart.TaskOnKart[str] = gokart.TaskInstanceParameter()
        int_task: gokart.TaskOnKart[int] = gokart.TaskInstanceParameter()
    
        def requires(self):
            return dict(str=self.str_task, int=self.int_task)
    
        def run(self):
            s = self.load(self.str_task)  # Inferred as str
            i = self.load(self.int_task)  # Inferred as int
    
    # Mypy validates that the tasks provided match the generic requirements
    SampleTask(
        str_task=StrTask(),  # OK
        int_task=StrTask(),  # Mypy error: expected TaskOnKart[int]
    ) 
  6. Define tasks using TaskOnKart

    master

    TaskOnKart is the primary class for defining tasks in gokart. It inherits from luigi.Task and provides type-safe methods for loading and dumping data. You define a task by specifying its type (e.g., TaskOnKart[str]), its requirements via requires(), its output location via output(), and its logic in run().

    By default, the output file path is inferred from the script's __name__, so the output() method can often be omitted.

    import luigi
    import gokart
    
    class TaskA(gokart.TaskOnKart[str]):
        param: luigi.Parameter = luigi.Parameter()
    
        def output(self):
            return self.make_target('output_of_task_a.pkl')
    
        def run(self):
            results = f'param={self.param}'
            self.dump(results)
    
    class TaskB(gokart.TaskOnKart[str]):
        param: luigi.Parameter = luigi.Parameter()
    
        def requires(self):
            return TaskA(param='world')
    
        def output(self):
            return self.make_target('output_of_task_b.pkl')
    
        def run(self):
            output_of_task_a = self.load()
            results = f'Task A: {output_of_task_a}\nTaskB: param={self.param}'
            self.dump(results)
    
    if __name__ == '__main__':
        print(gokart.build([TaskB(param='Hello')]))
  7. Rerun tasks in gokart

    master

    There are two primary ways to force a task to rerun in gokart, depending on whether you want to preserve the output file hash or generate a new one.

    1. Using the rerun parameter

    You can set rerun=True on a task instance. This forces the task to execute even if the output exists, but it does not change the hash value of the output file. This is useful for quick re-executions without changing the downstream dependency chain.

    class Main(GokartTask):
        def requires(self):
            return StringToSplit(task=Sample(rerun=True), rerun=True)

    2. Changing task parameters

    By adding or changing a parameter (e.g., a luigi.Parameter) on a task, you change its hash value. This causes the task to generate a new output file with a different name, which in turn triggers all dependent tasks to rerun because their inputs have changed.

    import luigi
    
    class Sample(GokartTask):
        version: luigi.IntParameter = luigi.IntParameter(default=1)
    
        def run(self):
            self.dump(f'sample output version {self.version}')

    Summary of differences:

    MethodChanges Hash?Affects Downstream?
    rerun=TrueNoNo (unless downstream is also forced)
    Parameter ChangeYesYes (automatically triggers downstream)
  8. Prevent task cache conflicts using Redis locks

    master

    When multiple worker nodes execute the same task, a race condition can occur where one node attempts to load a task's cache while another node is writing to it. This can result in reading corrupted or inappropriate data.

    To prevent these collisions, gokart.TaskOnKart supports a Redis-based locking mechanism. When enabled, every task inheriting from TaskOnKart will use a Redis server to coordinate access to cache files during dump or load operations, ensuring that no two nodes access the same cache file simultaneously.

  9. Understand gokart resource and log generation

    master

    Gokart automatically manages metadata for reproducibility. By default, a resources/ directory is created in the script's directory.

    Data Storage:

    • The task output is saved as a .pkl file inside a directory named after the module (__main__, etc.). The filename includes a hash based on task parameters, ensuring that changing parameters creates a new unique output file.

    Automatic Logs (log/ directory):

    • module_versions: Versions of all imported modules for reproducibility.
    • processing_time: Execution time of the task.
    • random_seed: Python and NumPy random seeds.
    • task_log: Output from the task logger.
    • task_params: The parameters used for the task.
  10. How to use pandas and Polars together in a pipeline

    master

    Gokart allows mixing pandas and Polars tasks in the same pipeline. This is useful for gradual migrations. When a Polars task requires a pandas task, use pl.from_pandas() to convert the loaded data.

    import pandas as pd
    import polars as pl
    from gokart import TaskOnKart
    from gokart.file_processor import FeatherFileProcessor
    
    class PandasTask(TaskOnKart[pd.DataFrame]):
        def output(self):
            return self.make_target(
                'path/to/pandas_output.feather',
                processor=FeatherFileProcessor(store_index_in_feather=False, dataframe_type='pandas')
            )
    
        def run(self):
            df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
            self.dump(df)
    
    class PolarsTask(TaskOnKart[pl.DataFrame]):
        def requires(self):
            return PandasTask()
    
        def output(self):
            return self.make_target(
                'path/to/polars_output.feather',
                processor=FeatherFileProcessor(store_index_in_feather=False, dataframe_type='polars')
            )
    
        def run(self):
            # Load pandas DataFrame and convert to Polars
            pandas_df = self.load()  # Returns pandas DataFrame
            polars_df = pl.from_pandas(pandas_df)
    
            # Process with Polars
            result = polars_df.with_columns((pl.col('a') * 2).alias('a_doubled'))
            self.dump(result)
  11. Configure the gokart Mypy plugin

    master

    To enable type checking for gokart tasks using Mypy, you must register the gokart.mypy plugin in your Mypy configuration file. This feature is currently experimental.

    If you use mypy.ini, add the plugin to the [mypy] section. If you use pyproject.toml, add it to the [tool.mypy] section.

    ### mypy.ini
    [mypy]
    plugins = gokart.mypy:plugin
    
    ### pyproject.toml
    [tool.mypy]
    plugins = ["gokart.mypy"]