Skill Tracer
Skills › Scientific Agent Skills › dask › best-practices.md

best-practices.md

281 lines · skills/dask/references/best-practices.md at 5f2bc03

Open on GitHub
1# Dask Best Practices
2
3Reviewed with Dask/distributed 2026.8.0. File paths, deployment settings, and undefined application functions are illustrative; executed local checks and current official sources are in [review.md](review.md).
4

Dask Best Practices

Reviewed with Dask/distributed 2026.8.0. File paths, deployment settings, and undefined application functions are illustrative; executed local checks and current official sources are in review.md.

Raw lines 1 to 4
5## Performance Optimization Principles
6

Performance Optimization Principles

Raw lines 5 to 6
7### Start with Simpler Solutions First
8
9Before implementing parallel computing with Dask, explore these alternatives:
10- Better algorithms for the specific problem
11- Efficient file formats (Parquet, HDF5, Zarr instead of CSV)
12- Compiled code via Numba or Cython
13- Data sampling for development and testing
15These alternatives often provide better returns than distributed systems and should be exhausted before scaling to parallel computing.

Start with Simpler Solutions First

Before implementing parallel computing with Dask, explore these alternatives:

  • Better algorithms for the specific problem
  • Efficient file formats (Parquet, HDF5, Zarr instead of CSV)
  • Compiled code via Numba or Cython
  • Data sampling for development and testing

These alternatives often provide better returns than distributed systems and should be exhausted before scaling to parallel computing.

Raw lines 7 to 16
17### Chunk Size Strategy
19**Critical Rule**: Chunks should be small enough that many fit in a worker's available memory at once.
21**Starting estimate**: Allow several chunks per active thread plus outputs, temporary buffers, Python overhead, and shuffle data. Ten chunks per core is a heuristic, not a safe universal limit.
23**Why It Matters**:
24- Too large chunks: Memory overflow and inefficient parallelization
25- Too small chunks: Excessive scheduling overhead
27**Example Calculation**:
28- 8 cores with 32 GB RAM
29- 400 MB per chunk (32 GB / 8 cores / 10) would consume the entire nominal budget; start smaller to leave workspace and OS headroom. Measure uncompressed memory, not file size.

Chunk Size Strategy

Critical Rule: Chunks should be small enough that many fit in a worker's available memory at once.

Starting estimate: Allow several chunks per active thread plus outputs, temporary buffers, Python overhead, and shuffle data. Ten chunks per core is a heuristic, not a safe universal limit.

Why It Matters:

  • Too large chunks: Memory overflow and inefficient parallelization
  • Too small chunks: Excessive scheduling overhead

Example Calculation:

  • 8 cores with 32 GB RAM
  • 400 MB per chunk (32 GB / 8 cores / 10) would consume the entire nominal budget; start smaller to leave workspace and OS headroom. Measure uncompressed memory, not file size.
Raw lines 17 to 30
31### Monitor with the Dashboard
33The Dask dashboard provides essential visibility into:
34- Worker states and resource utilization
35- Task progress and bottlenecks
36- Memory usage patterns
37- Performance characteristics
39Access the dashboard to understand what's actually slow in parallel workloads rather than guessing at optimizations.

Monitor with the Dashboard

The Dask dashboard provides essential visibility into:

  • Worker states and resource utilization
  • Task progress and bottlenecks
  • Memory usage patterns
  • Performance characteristics

Access the dashboard to understand what's actually slow in parallel workloads rather than guessing at optimizations.

Raw lines 31 to 40
41## Critical Pitfalls to Avoid

Critical Pitfalls to Avoid

Raw lines 41 to 42
43### 1. Don't Create Large Objects Locally Before Dask
45**Wrong Approach**:
46```python
47import pandas as pd
48import dask.dataframe as dd
50# Loads entire dataset into memory first
51df = pd.read_csv('large_file.csv')
52ddf = dd.from_pandas(df, npartitions=10)
53```
55**Correct Approach**:
56```python
57import dask.dataframe as dd
59# Let Dask handle the loading
60ddf = dd.read_csv('large_file.csv')
61```
63**Why**: Loading data with pandas or NumPy first forces the scheduler to serialize and embed those objects in task graphs, defeating the purpose of parallel computing.
65**Key Principle**: Use Dask methods to load data and use Dask to control the results.

1. Don't Create Large Objects Locally Before Dask

Wrong Approach:

import pandas as pd
import dask.dataframe as dd

# Loads entire dataset into memory first
df = pd.read_csv('large_file.csv')
ddf = dd.from_pandas(df, npartitions=10)

Correct Approach:

import dask.dataframe as dd

# Let Dask handle the loading
ddf = dd.read_csv('large_file.csv')

Why: Loading data with pandas or NumPy first forces the scheduler to serialize and embed those objects in task graphs, defeating the purpose of parallel computing.

Key Principle: Use Dask methods to load data and use Dask to control the results.

Raw lines 43 to 66
67### 2. Avoid Repeated compute() Calls
69**Wrong Approach**:
70```python
71results = []
72for item in items:
73 result = dask_computation(item).compute() # Each compute is separate
74 results.append(result)
75```
77**Correct Approach**:
78```python
79computations = [dask_computation(item) for item in items]
80results = dask.compute(*computations) # Single compute for all
81```
83**Why**: Calling compute in loops prevents Dask from:
84- Parallelizing different computations
85- Sharing intermediate results
86- Optimizing the overall task graph

2. Avoid Repeated compute() Calls

Wrong Approach:

results = []
for item in items:
    result = dask_computation(item).compute()  # Each compute is separate
    results.append(result)

Correct Approach:

computations = [dask_computation(item) for item in items]
results = dask.compute(*computations)  # Single compute for all

Why: Calling compute in loops prevents Dask from:

  • Parallelizing different computations
  • Sharing intermediate results
  • Optimizing the overall task graph
Raw lines 67 to 87
88### 3. Don't Build Excessively Large Task Graphs
90**Symptoms**:
91- Millions of tasks in a single computation
92- Severe scheduling overhead
93- Long delays before computation starts
95**Solutions**:
96- Increase chunk sizes to reduce number of tasks
97- Use `map_partitions` or `map_blocks` to fuse operations
98- Break computations into smaller pieces with intermediate persists
99- Consider whether the problem truly requires distributed computing
101**Example Using map_partitions**:
102```python
103# Instead of applying function to each row
104ddf['result'] = ddf.apply(complex_function, axis=1, meta=('result', 'float64'))
105# Python row calls within partitions, not one Dask task per row
107# Apply to entire partitions at once
108ddf = ddf.map_partitions(lambda df: df.assign(result=complex_function(df)))
109```

3. Don't Build Excessively Large Task Graphs

Symptoms:

  • Millions of tasks in a single computation
  • Severe scheduling overhead
  • Long delays before computation starts

Solutions:

  • Increase chunk sizes to reduce number of tasks
  • Use map_partitions or map_blocks to fuse operations
  • Break computations into smaller pieces with intermediate persists
  • Consider whether the problem truly requires distributed computing

Example Using map_partitions:

# Instead of applying function to each row
ddf['result'] = ddf.apply(complex_function, axis=1, meta=('result', 'float64'))
# Python row calls within partitions, not one Dask task per row

# Apply to entire partitions at once
ddf = ddf.map_partitions(lambda df: df.assign(result=complex_function(df)))
Raw lines 88 to 110
111## Infrastructure Considerations

Infrastructure Considerations

Raw lines 111 to 112
113### Scheduler Selection
115**Use Threads For**:
116- Numeric work with GIL-releasing libraries (NumPy, Pandas, scikit-learn)
117- Operations that benefit from shared memory
118- Single-machine workloads with array/dataframe operations
120**Use Processes For**:
121- Text processing and Python collection operations
122- Pure Python code that's GIL-bound
123- Operations that need process isolation
125**Use Distributed Scheduler For**:
126- Multi-machine clusters
127- Need for diagnostic dashboard
128- Asynchronous APIs
129- Better data locality handling

Scheduler Selection

Use Threads For:

  • Numeric work with GIL-releasing libraries (NumPy, Pandas, scikit-learn)
  • Operations that benefit from shared memory
  • Single-machine workloads with array/dataframe operations

Use Processes For:

  • Text processing and Python collection operations
  • Pure Python code that's GIL-bound
  • Operations that need process isolation

Use Distributed Scheduler For:

  • Multi-machine clusters
  • Need for diagnostic dashboard
  • Asynchronous APIs
  • Better data locality handling
Raw lines 113 to 130
131### Thread Configuration
133**Recommendation**: Aim for roughly 4 threads per process on numeric workloads.
135**Rationale**:
136- Balance between parallelism and overhead
137- Allows efficient use of CPU cores
138- Reduces context switching costs

Thread Configuration

Recommendation: Aim for roughly 4 threads per process on numeric workloads.

Rationale:

  • Balance between parallelism and overhead
  • Allows efficient use of CPU cores
  • Reduces context switching costs
Raw lines 131 to 139
140### Memory Management
142**Persist Strategically**: Persist reusable intermediates only after filtering and selecting columns; persistence blocks later optimizer pushdown. Distributed persistence stays on workers, while local persistence occupies local memory. It is not an automatic cure for memory errors.
143```python
144# Persist intermediate results that are reused
145intermediate = expensive_computation(data).persist()
146result1 = intermediate.operation1().compute()
147result2 = intermediate.operation2().compute()
148```
150**Clear Memory When Done**:
151```python
152# Explicitly delete large objects
153del intermediate
154```

Memory Management

Persist Strategically: Persist reusable intermediates only after filtering and selecting columns; persistence blocks later optimizer pushdown. Distributed persistence stays on workers, while local persistence occupies local memory. It is not an automatic cure for memory errors.

# Persist intermediate results that are reused
intermediate = expensive_computation(data).persist()
result1 = intermediate.operation1().compute()
result2 = intermediate.operation2().compute()

Clear Memory When Done:

# Explicitly delete large objects
del intermediate
Raw lines 140 to 155
156## Data Loading Best Practices

Data Loading Best Practices

Raw lines 156 to 157
158### Use Appropriate File Formats
160**For Tabular Data**:
161- Parquet: Columnar, compressed, fast filtering
162- CSV: Only for small data or initial ingestion
164**For Array Data**:
165- HDF5: Good for numeric arrays
166- Zarr: Cloud-native, parallel-friendly
167- NetCDF: Scientific data with metadata

Use Appropriate File Formats

For Tabular Data:

  • Parquet: Columnar, compressed, fast filtering
  • CSV: Only for small data or initial ingestion

For Array Data:

  • HDF5: Good for numeric arrays
  • Zarr: Cloud-native, parallel-friendly
  • NetCDF: Scientific data with metadata
Raw lines 158 to 168
169### Optimize Data Ingestion
171**Read Multiple Files Efficiently**:
172```python
173# Use glob patterns to read multiple files in parallel
174ddf = dd.read_parquet('data/year=2024/month=*/day=*.parquet')
175```
177**Specify Useful Columns Early**:
178```python
179# Only read needed columns
180ddf = dd.read_parquet('data.parquet', columns=['col1', 'col2', 'col3'])
181```

Optimize Data Ingestion

Read Multiple Files Efficiently:

# Use glob patterns to read multiple files in parallel
ddf = dd.read_parquet('data/year=2024/month=*/day=*.parquet')

Specify Useful Columns Early:

# Only read needed columns
ddf = dd.read_parquet('data.parquet', columns=['col1', 'col2', 'col3'])
Raw lines 169 to 182
183## Common Patterns and Solutions

Common Patterns and Solutions

Raw lines 183 to 184
185### Pattern: Embarrassingly Parallel Problems
187For independent computations, use Futures:
188```python
189from dask.distributed import Client
191client = Client()
192futures = [client.submit(func, arg) for arg in args]
193results = client.gather(futures)
194```

Pattern: Embarrassingly Parallel Problems

For independent computations, use Futures:

from dask.distributed import Client

client = Client()
futures = [client.submit(func, arg) for arg in args]
results = client.gather(futures)
Raw lines 185 to 195
196### Pattern: Data Preprocessing Pipeline
198Use Bags for initial ETL, then convert to structured formats:
199```python
200import dask.bag as db
201import json
203# Process raw JSON Lines
204bag = db.read_text('logs/*.json').map(json.loads)
205bag = bag.filter(lambda x: x['status'] == 'success')
207# Convert to DataFrame for analysis
208ddf = bag.to_dataframe()
209```

Pattern: Data Preprocessing Pipeline

Use Bags for initial ETL, then convert to structured formats:

import dask.bag as db
import json

# Process raw JSON Lines
bag = db.read_text('logs/*.json').map(json.loads)
bag = bag.filter(lambda x: x['status'] == 'success')

# Convert to DataFrame for analysis
ddf = bag.to_dataframe()
Raw lines 196 to 210
211### Pattern: Iterative Algorithms
213Persist data between iterations:
214```python
215data = dd.read_parquet('data.parquet')
216data = data.persist() # Only when reusable partitions fit worker memory
218for iteration in range(num_iterations):
219 data = update_function(data)
220 data = data.persist() # Persist updated version
221```

Pattern: Iterative Algorithms

Persist data between iterations:

data = dd.read_parquet('data.parquet')
data = data.persist()  # Only when reusable partitions fit worker memory

for iteration in range(num_iterations):
    data = update_function(data)
    data = data.persist()  # Persist updated version
Raw lines 211 to 222
223## Debugging Tips

Debugging Tips

Raw lines 223 to 224
225### Use Single-Threaded Scheduler
227For debugging with pdb or detailed error inspection:
228```python
229import dask
231dask.config.set(scheduler='synchronous')
232result = computation.compute() # Runs in single thread for debugging
233```

Use Single-Threaded Scheduler

For debugging with pdb or detailed error inspection:

import dask

dask.config.set(scheduler='synchronous')
result = computation.compute()  # Runs in single thread for debugging
Raw lines 225 to 234
235### Check Task Graph Size
237Before computing, check the number of tasks:
238```python
239print(len(ddf.__dask_graph__())) # Should be reasonable, not millions
240```

Check Task Graph Size

Before computing, check the number of tasks:

print(len(ddf.__dask_graph__()))  # Should be reasonable, not millions
Raw lines 235 to 241
242### Validate on Small Data First
244Test logic on small subset before scaling:
245```python
246# Test on first partition
247sample = ddf.head(1000)
248# Validate results
249# Then scale to full dataset
250```

Validate on Small Data First

Test logic on small subset before scaling:

# Test on first partition
sample = ddf.head(1000)
# Validate results
# Then scale to full dataset
Raw lines 242 to 251
252## Performance Troubleshooting

Performance Troubleshooting

Raw lines 252 to 253
254### Symptom: Slow Computation Start
256**Likely Cause**: Task graph is too large
257**Solution**: Increase chunk sizes or use map_partitions

Symptom: Slow Computation Start

Likely Cause: Task graph is too large Solution: Increase chunk sizes or use map_partitions

Raw lines 254 to 258
259### Symptom: Memory Errors
261**Likely Causes**:
262- Chunks too large
263- Too many intermediate results
264- Memory leaks in user functions
266**Solutions**:
267- Decrease chunk sizes
268- Use persist() strategically and delete when done
269- Profile user functions for memory issues

Symptom: Memory Errors

Likely Causes:

  • Chunks too large
  • Too many intermediate results
  • Memory leaks in user functions

Solutions:

  • Decrease chunk sizes
  • Use persist() strategically and delete when done
  • Profile user functions for memory issues
Raw lines 259 to 270
271### Symptom: Poor Parallelization
273**Likely Causes**:
274- Data dependencies preventing parallelism
275- Chunks too large (not enough tasks)
276- GIL contention with threads on Python code
278**Solutions**:
279- Restructure computation to reduce dependencies
280- Increase number of partitions
281- Switch to multiprocessing scheduler for Python code

Symptom: Poor Parallelization

Likely Causes:

  • Data dependencies preventing parallelism
  • Chunks too large (not enough tasks)
  • GIL contention with threads on Python code

Solutions:

  • Restructure computation to reduce dependencies
  • Increase number of partitions
  • Switch to multiprocessing scheduler for Python code
Raw lines 271 to 281