Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn on potential data races #1541

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
46b4813
Write tests with mapped tasklets
luca-patrignani Mar 3, 2024
922d5ef
Add config flag
luca-patrignani Mar 3, 2024
bf1b814
Check if there are intersecting memlet subsets for a given access node
luca-patrignani Mar 4, 2024
b928aa4
Filter only UserWarning in test_memlet_range_not_overlap_ranges
luca-patrignani Mar 4, 2024
19b4a03
Update dace/config_schema.yml
luca-patrignani Mar 11, 2024
653b0de
Move check_race_conditions flag into experimental category and add test
luca-patrignani Mar 11, 2024
8534fb3
Create test with overlapping ranges with two different access nodes
luca-patrignani Mar 12, 2024
3469315
Create test for symbolic overlap
luca-patrignani Mar 12, 2024
87cf81e
Create tests for constant memlet overlap and almost overlap
luca-patrignani Mar 12, 2024
8cbf45f
Revert changes to submodule
luca-patrignani Mar 12, 2024
8120d39
Fix constant_memlet_almost_overlap_test
luca-patrignani Mar 14, 2024
7f7f863
Fix constant_memlet_almost_overlap_test for real
luca-patrignani Mar 14, 2024
ff497ea
Check write-write data races and read-write data races
luca-patrignani Mar 14, 2024
4b5ac97
Remove are_intesecting function
luca-patrignani Mar 15, 2024
1c8d803
Validate sdfgs instead of running the programs
luca-patrignani Mar 16, 2024
655fb90
Use is True instead of first checking if it's null and then check if …
luca-patrignani Mar 16, 2024
a849a84
Add test_elementwise_map
luca-patrignani Mar 16, 2024
2f8f60c
Add test for wcr
luca-patrignani Mar 17, 2024
7293fa9
Check if there are paths between the two nodes when looking for
luca-patrignani Mar 18, 2024
322b256
Remove unused numpy import
luca-patrignani Mar 18, 2024
bacca88
Merge branch 'master' into master
phschaad May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions dace/config_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ required:
be called before every compiled SDFG's generated code is invoked. Used
for functionality such as low-level profiling.


#############################################
# Experimental features

Expand All @@ -919,6 +920,12 @@ required:
description: >
Check for undefined symbols in memlets during SDFG validation.

check_race_conditions:
type: bool
default: false
title: Check race conditions
description: Check for probable race conditions during validation.

#############################################
# Features for unit testing

Expand Down
29 changes: 29 additions & 0 deletions dace/sdfg/validation.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Copyright 2019-2021 ETH Zurich and the DaCe authors. All rights reserved.
""" Exception classes and methods for validation of SDFGs. """
from collections import defaultdict
import copy
from dace.dtypes import DebugInfo
import os
from typing import TYPE_CHECKING, Dict, List, Set
import warnings
from dace import dtypes, subsets
from dace import symbolic
from dace.sdfg.nodes import AccessNode

if TYPE_CHECKING:
import dace
Expand Down Expand Up @@ -790,6 +792,33 @@ def validate_state(state: 'dace.sdfg.SDFGState',
continue
raise error

if Config.get_bool("experimental.check_race_conditions"):
node_labels = []
write_accesses = defaultdict(list)
read_accesses = defaultdict(list)
for node in state.data_nodes():
node_labels.append(node.label)
write_accesses[node.label].extend([e.data.dst_subset for e in state.in_edges(node)])
read_accesses[node.label].extend([e.data.src_subset for e in state.out_edges(node)])

for node_label in node_labels:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few notes on this test:

  1. You should keep the actual access nodes in your dictionary. If a map that reads from A[i] also writes to A[i], that would trigger a validation warning incorrectly. Instead you need to check if:
  • The node is the same (in that case, write-write conflicts may still happen)
  • If the nodes are not the same, whether there is a path from the first to the second (use has_path to check that). If there is a path, then there is no conflict (read/write or write/write). This is another reason for this check being expensive. There may be some analysis passes in dace.transformation.passes that give you the paths between dependent access nodes, but creating a dictionary would be an optimization. To be safe you can use has_path for now and leave a comment that this could be optimized.
  1. Write-conflict resolution memlets (memlet.wcr is not None) are exempt from write-write conflicts (maybe not read-write)

Please also add an elementwise map test (A->mapentry-A[i]->tasklet-A[i]->mapexit->A) just to make sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to follow your notes but I felt kind of lost, especially for point 2. Consider this two test cases:

def test1():
    sdfg = dace.SDFG('memlet_overlap_with_wcr')
    state = sdfg.add_state()
    sdfg.add_array("A", (20,), dace.int32)
    sdfg.add_array("B", (1,), dace.int32)
    A = state.add_read("A")
    B = state.add_write("B")

    state.add_mapped_tasklet(
        name="first_reduction",
        code="b = a",
        inputs={"a": dace.Memlet(data="A", subset="k")},
        outputs={"b": dace.Memlet(data="B", subset="0", wcr="lambda old, new: old + new")},
        map_ranges={"k": "0:20"},
        external_edges=True,
        input_nodes={"A": A},
        output_nodes={"B": B}
    )

    state.add_mapped_tasklet(
        name="second_reduction",
        code="b = a",
        inputs={"a": dace.Memlet(data="A", subset="k")},
        outputs={"b": dace.Memlet(data="B", subset="0", wcr="lambda old, new: old * new")},
        map_ranges={"k": "0:20"},
        external_edges=True,
        input_nodes={"A": A},
        output_nodes={"B": B}
    )

    with warnings.catch_warnings():
        warnings.simplefilter("error", UserWarning)
        with dace.config.set_temporary('experimental', 'check_race_conditions', value=True):
            sdfg.validate()

def test2():
    sdfg = dace.SDFG('memlet_overlap_with_wcr')
    state = sdfg.add_state()
    sdfg.add_array("A", (20,), dace.int32)
    sdfg.add_array("B", (1,), dace.int32)
    A = state.add_read("A")
    B = state.add_write("B")

    state.add_mapped_tasklet(
        name="first_reduction",
        code="b = a",
        inputs={"a": dace.Memlet(data="A", subset="k")},
        outputs={"b": dace.Memlet(data="B", subset="0", wcr="lambda old, new: old + new")},
        map_ranges={"k": "0:20"},
        external_edges=True,
        input_nodes={"A": A},
        output_nodes={"B": B}
    )

    state.add_mapped_tasklet(
        name="second_reduction",
        code="b = a",
        inputs={"a": dace.Memlet(data="A", subset="k")},
        outputs={"b": dace.Memlet(data="B", subset="0")},
        map_ranges={"k": "0:20"},
        external_edges=True,
        input_nodes={"A": A},
        output_nodes={"B": B}
    )

    with warnings.catch_warnings():
        warnings.simplefilter("error", UserWarning)
        with dace.config.set_temporary('experimental', 'check_race_conditions', value=True):
            sdfg.validate()

Are they considered data races? At this moment my code raises no warnings.
I'm probably missing something important, sorry for this long PR review.

write_memlet_subsets = write_accesses[node_label]
read_memlet_subsets = read_accesses[node_label]
# check write-write data races
for i in range(len(write_memlet_subsets)):
for j in range(i+1, len(write_memlet_subsets)):
intersects = subsets.intersects(write_memlet_subsets[i], write_memlet_subsets[j])
if intersects is not None:
if intersects:
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved
warnings.warn(f'Memlet range overlap while writing to "{node}" in state "{state.label}"')
# check read-write data races
for write in write_memlet_subsets:
for read in read_memlet_subsets:
intersects = subsets.intersects(write, read)
if intersects is not None:
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved
if intersects:
warnings.warn(f'Memlet range overlap while writing to "{node}" in state "{state.label}"')

########################################


Expand Down
285 changes: 285 additions & 0 deletions tests/warn_on_potential_data_race_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
# Copyright 2019-2024 ETH Zurich and the DaCe authors. All rights reserved.

import warnings
import dace
import numpy as np
import pytest

luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved

def test_memlet_range_not_overlap_ranges():
sdfg = dace.SDFG('memlet_range_not_overlap_ranges')
state = sdfg.add_state()
N = dace.symbol("N", dtype=dace.int32)
sdfg.add_array("A", (N//2,), dace.int32)
A = state.add_access("A")
sdfg.add_array("B", (N,), dace.int32)
B = state.add_access("B")
state.add_mapped_tasklet(
name="first_tasklet",
code="b = a + 10",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "0:N//2"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)
state.add_mapped_tasklet(
name="second_tasklet",
code="b = a - 20",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k+N//2")},
map_ranges={"k": "0:N//2"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)

N = 6
A = np.arange(N//2, dtype=np.int32)
B = np.zeros((N,), dtype=np.int32)
with warnings.catch_warnings():
warnings.simplefilter("error", UserWarning)
with dace.config.set_temporary("experimental.check_race_conditions", value=True):
sdfg(N=N, A=A, B=B)
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved


def test_memlet_range_write_write_overlap_ranges():
sdfg = dace.SDFG('memlet_range_overlap_ranges')
state = sdfg.add_state()
N = dace.symbol("N", dtype=dace.int32)
sdfg.add_array("A", (N,), dace.int32)
A = state.add_access("A")
sdfg.add_array("B", (N,), dace.int32)
B = state.add_access("B")
state.add_mapped_tasklet(
name="first_tasklet",
code="b = a + 10",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "0:N"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)
state.add_mapped_tasklet(
name="second_tasklet",
code="b = a - 20",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "0:N"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)

N = 6
A = np.arange(N, dtype=np.int32)
B = np.zeros((N,), dtype=np.int32)
with pytest.warns(UserWarning):
with dace.config.set_temporary("experimental.check_race_conditions", value=True):
sdfg(N=N, A=A, B=B)
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved

def test_memlet_range_write_read_overlap_ranges():
sdfg = dace.SDFG('memlet_range_write_read_overlap_ranges')
state = sdfg.add_state()
N = dace.symbol("N", dtype=dace.int32)
sdfg.add_array("A", (N,), dace.int32)
A_read = state.add_read("A")
A_write = state.add_write("A")
sdfg.add_array("B", (N,), dace.int32)
B = state.add_access("B")
sdfg.add_array("C", (N,), dace.int32)
C = state.add_access("C")
state.add_mapped_tasklet(
name="first_tasklet",
code="b = a + 10",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "0:N"},
external_edges=True,
input_nodes={"A": A_read},
output_nodes={"B": B}
)
state.add_mapped_tasklet(
name="second_tasklet",
code="a = c - 20",
inputs={"c": dace.Memlet(data="C", subset="k")},
outputs={"a": dace.Memlet(data="A", subset="k")},
map_ranges={"k": "0:N"},
external_edges=True,
input_nodes={"C": C},
output_nodes={"A": A_write}
)

N = 6
A = np.arange(N, dtype=np.int32)
B = np.zeros((N,), dtype=np.int32)
C = 20 * A

with pytest.warns(UserWarning):
with dace.config.set_temporary('experimental', 'check_race_conditions', value=True):
sdfg(N=N, A=A, B=B, C=C)
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved

def test_memlet_overlap_ranges_two_access_nodes():
sdfg = dace.SDFG('memlet_range_write_read_overlap_ranges')
state = sdfg.add_state()
N = dace.symbol("N", dtype=dace.int32)
sdfg.add_array("A", (N,), dace.int32)
A1 = state.add_access("A")
A2 = state.add_access("A")
sdfg.add_array("B", (N,), dace.int32)
B1 = state.add_access("B")
B2 = state.add_access("B")

state.add_mapped_tasklet(
name="first_tasklet",
code="b = a + 10",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "0:N"},
external_edges=True,
input_nodes={"A": A1},
output_nodes={"B": B1}
)
state.add_mapped_tasklet(
name="second_tasklet",
code="b = a - 20",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "0:N"},
external_edges=True,
input_nodes={"A": A2},
output_nodes={"B": B2}
)

N = 6
A = np.arange(N, dtype=np.int32)
B = np.zeros((N,), dtype=np.int32)
C = 20 * A

with pytest.warns(UserWarning):
with dace.config.set_temporary('experimental', 'check_race_conditions', value=True):
sdfg(N=N, A=A, B=B, C=C)
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved

def test_memlet_overlap_symbolic_ranges():
sdfg = dace.SDFG('memlet_overlap_symbolic_ranges')
state = sdfg.add_state()
N = dace.symbol("N", dtype=dace.int32)
sdfg.add_array("A", (2*N,), dace.int32)
A = state.add_access("A")
sdfg.add_array("B", (2*N,), dace.int32)
B = state.add_access("B")

state.add_mapped_tasklet(
name="first_tasklet",
code="b = a + 10",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "0:N"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)
state.add_mapped_tasklet(
name="second_tasklet",
code="b = a - 20",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "0:2*N"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)

N = 6
A = np.arange(2*N, dtype=np.int32)
B = np.zeros((2*N,), dtype=np.int32)
C = 20 * A

with pytest.warns(UserWarning):
with dace.config.set_temporary('experimental', 'check_race_conditions', value=True):
sdfg(N=N, A=A, B=B, C=C)
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved

def test_constant_memlet_overlap():
sdfg = dace.SDFG('constant_memlet_overlap')
state = sdfg.add_state()
sdfg.add_array("A", (12,), dace.int32)
A = state.add_access("A")
sdfg.add_array("B", (12,), dace.int32)
B = state.add_access("B")

state.add_mapped_tasklet(
name="first_tasklet",
code="b = a + 10",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "3:10"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)
state.add_mapped_tasklet(
name="second_tasklet",
code="b = a - 20",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "6:12"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)

A = np.arange(12, dtype=np.int32)
B = np.zeros((12,), dtype=np.int32)

with pytest.warns(UserWarning):
with dace.config.set_temporary('experimental', 'check_race_conditions', value=True):
sdfg(A=A, B=B)
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved

def test_constant_memlet_almost_overlap():
sdfg = dace.SDFG('constant_memlet_almost_overlap')
state = sdfg.add_state()
sdfg.add_array("A", (20,), dace.int32)
A = state.add_access("A")
sdfg.add_array("B", (20,), dace.int32)
B = state.add_access("B")

state.add_mapped_tasklet(
name="first_tasklet",
code="b = a + 10",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "3:10"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)
state.add_mapped_tasklet(
name="second_tasklet",
code="b = a - 20",
inputs={"a": dace.Memlet(data="A", subset="k")},
outputs={"b": dace.Memlet(data="B", subset="k")},
map_ranges={"k": "10:20"},
external_edges=True,
input_nodes={"A": A},
output_nodes={"B": B}
)

A = np.arange(20, dtype=np.int32)
B = np.zeros((20,), dtype=np.int32)

with warnings.catch_warnings():
warnings.simplefilter("error", UserWarning)
with dace.config.set_temporary('experimental', 'check_race_conditions', value=True):
sdfg(A=A, B=B)
luca-patrignani marked this conversation as resolved.
Show resolved Hide resolved


if __name__ == '__main__':
test_memlet_range_not_overlap_ranges()
test_memlet_range_write_write_overlap_ranges()
test_memlet_range_write_read_overlap_ranges()
test_memlet_overlap_ranges_two_access_nodes()
test_memlet_overlap_symbolic_ranges()
test_constant_memlet_overlap()
test_constant_memlet_almost_overlap()