Compare commits
20 Commits
performanc
...
doc
Author | SHA1 | Date | |
---|---|---|---|
1e45ade98b | |||
e4f2ae5b85 | |||
8a6e3864eb | |||
42076c4576 | |||
3a72279b6b | |||
ad05ada3e3 | |||
cf19856118 | |||
e07ade47ad | |||
7110007817 | |||
ba97086983 | |||
01c1b5082f | |||
122e428efe | |||
58c042ccc7 | |||
3cdc75d3f6 | |||
8014bbffcd | |||
ae1345d547 | |||
dbcd569967 | |||
0f5f475cb4 | |||
1b4030d633 | |||
383c92ec47 |
13
.JuliaFormatter.toml
Normal file
13
.JuliaFormatter.toml
Normal file
@ -0,0 +1,13 @@
|
||||
indent = 4
|
||||
margin = 80
|
||||
always_for_in = true
|
||||
for_in_replacement = "in"
|
||||
whitespace_typedefs = true
|
||||
whitespace_ops_in_indices = true
|
||||
long_to_short_function_def = false
|
||||
always_use_return = true
|
||||
whitespace_in_kwargs = true
|
||||
conditional_to_if = true
|
||||
normalize_line_endings = "unix"
|
||||
|
||||
overwrite = true
|
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,2 +1,2 @@
|
||||
examples/AB->ABBBBBBB.txt filter=lfs diff=lfs merge=lfs -text
|
||||
examples/AB->ABBBBBBBBB.txt filter=lfs diff=lfs merge=lfs -text
|
||||
input/AB->ABBBBBBBBB.txt filter=lfs diff=lfs merge=lfs -text
|
||||
input/AB->ABBBBBBB.txt filter=lfs diff=lfs merge=lfs -text
|
||||
|
@ -1,33 +1,185 @@
|
||||
name: Test
|
||||
name: MetagraphOptimization_CI
|
||||
|
||||
on: [push]
|
||||
|
||||
env:
|
||||
# keep the depot directly in the repository for the cache
|
||||
JULIA_DEPOT_PATH: './.julia'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
prepare:
|
||||
runs-on: arch-latest
|
||||
|
||||
steps:
|
||||
#- name: Get git-lfs
|
||||
# run: apt-get update && apt-get install git-lfs
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
#- name: Checkout LFS objects
|
||||
# run: git lfs checkout
|
||||
|
||||
- name: Setup Julia environment
|
||||
uses: https://github.com/julia-actions/setup-julia@v1.9.1
|
||||
uses: https://github.com/julia-actions/setup-julia@v1.9.2
|
||||
with:
|
||||
version: '1.9.1'
|
||||
version: '1.9.2'
|
||||
|
||||
# needed for the file hashing, should be removed when ${{ hashFiles('**/Project.toml') }} is supported in gitea
|
||||
- name: Setup go environment
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.20'
|
||||
|
||||
- name: Hash files
|
||||
uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||
id: get-hash
|
||||
with:
|
||||
patterns: |-
|
||||
**/Project.toml
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v3
|
||||
id: cache-restore
|
||||
with:
|
||||
path: |
|
||||
.julia/artifacts
|
||||
.julia/packages
|
||||
.julia/registries
|
||||
key: julia-${{ steps.get-hash.outputs.hash }}
|
||||
|
||||
- name: Check cache hit
|
||||
if: steps.cache-restore.outputs.cache-hit == 'true'
|
||||
run: exit 0
|
||||
|
||||
- name: Install dependencies
|
||||
run: julia --project=./ -e 'import Pkg; Pkg.instantiate()'
|
||||
run: |
|
||||
julia --project=./ -e 'import Pkg; Pkg.instantiate(); Pkg.precompile()'
|
||||
julia --project=examples/ -e 'import Pkg; Pkg.develop(Pkg.PackageSpec(path=pwd())); Pkg.instantiate(); Pkg.precompile()'
|
||||
julia --project=docs/ -e 'import Pkg; Pkg.develop(Pkg.PackageSpec(path=pwd())); Pkg.instantiate(); Pkg.precompile()'
|
||||
|
||||
- name: Cache Julia packages
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
.julia/artifacts
|
||||
.julia/packages
|
||||
.julia/registries
|
||||
key: julia-${{ steps.get-hash.outputs.hash }}
|
||||
|
||||
test:
|
||||
needs: prepare
|
||||
runs-on: arch-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Julia environment
|
||||
uses: https://github.com/julia-actions/setup-julia@v1.9.2
|
||||
with:
|
||||
version: '1.9.2'
|
||||
|
||||
# needed for the file hashing, should be removed when ${{ hashFiles('**/Project.toml') }} is supported in gitea
|
||||
- name: Setup go environment
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.20'
|
||||
|
||||
- name: Hash files
|
||||
uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||
id: get-hash
|
||||
with:
|
||||
patterns: |-
|
||||
**/Project.toml
|
||||
|
||||
- name: Restore cached Julia packages
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
.julia/artifacts
|
||||
.julia/packages
|
||||
.julia/registries
|
||||
key: julia-${{ steps.get-hash.outputs.hash }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
julia --project=./ -e 'import Pkg; Pkg.instantiate(); Pkg.precompile()'
|
||||
julia --project=examples/ -e 'import Pkg; Pkg.develop(Pkg.PackageSpec(path=pwd())); Pkg.instantiate(); Pkg.precompile()'
|
||||
julia --project=docs/ -e 'import Pkg; Pkg.develop(Pkg.PackageSpec(path=pwd())); Pkg.instantiate(); Pkg.precompile()'
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
julia --project=./ -e 'using JuliaFormatter; format(".", verbose=true)'
|
||||
julia --project=./ -e '
|
||||
out = Cmd(`git diff --name-only`) |> read |> String
|
||||
if out == ""
|
||||
exit(0)
|
||||
else
|
||||
@error "Some files have not been formatted !!!"
|
||||
write(stdout, out)
|
||||
exit(1)
|
||||
end'
|
||||
|
||||
- name: Run tests
|
||||
run: julia --project=./ -t 4 -e 'import Pkg; Pkg.test()' -O0
|
||||
|
||||
- name: Run examples
|
||||
run: julia --project=examples/ -t 4 -e 'import Pkg; Pkg.develop(Pkg.PackageSpec(path=pwd())); Pkg.instantiate(); include("examples/import_bench.jl")' -O3
|
||||
run: julia --project=examples/ -t 4 -e 'include("examples/import_bench.jl")' -O3
|
||||
|
||||
docs:
|
||||
needs: prepare
|
||||
runs-on: arch-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Julia environment
|
||||
uses: https://github.com/julia-actions/setup-julia@v1.9.2
|
||||
with:
|
||||
version: '1.9.2'
|
||||
|
||||
# needed for the file hashing, should be removed when ${{ hashFiles('**/Project.toml') }} is supported in gitea
|
||||
- name: Setup go environment
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.20'
|
||||
|
||||
- name: Hash files
|
||||
uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||
id: get-hash
|
||||
with:
|
||||
patterns: |-
|
||||
**/Project.toml
|
||||
|
||||
- name: Restore cached Julia packages
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
.julia/artifacts
|
||||
.julia/packages
|
||||
.julia/registries
|
||||
key: julia-${{ steps.get-hash.outputs.hash }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
julia --project=./ -e 'import Pkg; Pkg.instantiate(); Pkg.precompile()'
|
||||
julia --project=examples/ -e 'import Pkg; Pkg.develop(Pkg.PackageSpec(path=pwd())); Pkg.instantiate(); Pkg.precompile()'
|
||||
julia --project=docs/ -e 'import Pkg; Pkg.develop(Pkg.PackageSpec(path=pwd())); Pkg.instantiate(); Pkg.precompile()'
|
||||
|
||||
- name: Build docs
|
||||
run: julia --project=docs/ docs/make.jl
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: web-doc
|
||||
path: docs/build/
|
||||
|
||||
#- name: Webhook Trigger
|
||||
# uses: https://github.com/zzzze/webhook-trigger@master
|
||||
# continue-on-error: true
|
||||
# with:
|
||||
# data: "{\"event\":\"action_completed\", \"download_url\":\"deckardcain.local:8099/something\"}"
|
||||
# webhook_url: ${{ secrets.WEBHOOK_URL }}
|
||||
|
@ -5,7 +5,7 @@ version = "0.1.0"
|
||||
|
||||
[deps]
|
||||
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
||||
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
|
||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
|
||||
|
4
docs/Project.toml
Normal file
4
docs/Project.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[deps]
|
||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||
DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8"
|
||||
MetagraphOptimization = "3e869610-d48d-4942-ba70-c1b702a33ca4"
|
29
docs/make.jl
Normal file
29
docs/make.jl
Normal file
@ -0,0 +1,29 @@
|
||||
using Documenter
|
||||
using MetagraphOptimization
|
||||
|
||||
makedocs(
|
||||
#format = Documenter.LaTeX(platform=""),
|
||||
|
||||
root = "docs",
|
||||
source = "src",
|
||||
build = "build",
|
||||
clean = true,
|
||||
doctest = true,
|
||||
modules = Module[MetagraphOptimization],
|
||||
repo = "https://code.woubery.com/Rubydragon/MetagraphOptimization.jl/src/branch/{commit}{path}#L{line}",
|
||||
sitename = "MetagraphOptimization.jl",
|
||||
pages = [
|
||||
"index.md",
|
||||
"Manual" => "manual.md",
|
||||
"Library" => [
|
||||
"Graph" => "lib/internals/graph.md",
|
||||
"Node" => "lib/internals/node.md",
|
||||
"Task" => "lib/internals/task.md",
|
||||
"Operation" => "lib/internals/operation.md",
|
||||
"Models" => "lib/internals/models.md",
|
||||
"Diff" => "lib/internals/diff.md",
|
||||
"Utility" => "lib/internals/utility.md",
|
||||
],
|
||||
"Contribution" => "contribution.md",
|
||||
],
|
||||
)
|
3
docs/src/contribution.md
Normal file
3
docs/src/contribution.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Contribution
|
||||
|
||||
This is currently in development for a diploma thesis and is therefore private and impossible to contribute to.
|
26
docs/src/index.md
Normal file
26
docs/src/index.md
Normal file
@ -0,0 +1,26 @@
|
||||
# MetagraphOptimization.jl
|
||||
|
||||
*A domain-specific DAG-optimizer*
|
||||
|
||||
## Package Features
|
||||
- Read a DAG from a file
|
||||
- Analyze its properties
|
||||
- Mute the graph using the operations NodeFusion, NodeReduction and NodeSplit
|
||||
|
||||
## Coming Soon:
|
||||
- Add Code Generation from finished DAG
|
||||
- Add optimization algorithms and strategies
|
||||
|
||||
## Library Outline
|
||||
|
||||
```@contents
|
||||
Pages = [
|
||||
"lib/public.md",
|
||||
"lib/internals.md"
|
||||
]
|
||||
```
|
||||
|
||||
### [Index](@id main-index)
|
||||
```@index
|
||||
Pages = ["lib/public.md"]
|
||||
```
|
22
docs/src/lib/internals/diff.md
Normal file
22
docs/src/lib/internals/diff.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Diff
|
||||
|
||||
## Type
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["diff/type.jl"]
|
||||
Order = [:type]
|
||||
```
|
||||
|
||||
## Properties
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["diff/properties.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Printing
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["diff/print.jl"]
|
||||
Order = [:function]
|
||||
```
|
50
docs/src/lib/internals/graph.md
Normal file
50
docs/src/lib/internals/graph.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Graph
|
||||
|
||||
## Type
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["graph/type.jl"]
|
||||
Order = [:type]
|
||||
```
|
||||
|
||||
## Interface
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["graph/interface.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Compare
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["graph/compare.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Mute
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["graph/mute.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Print
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["graph/print.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Properties
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["graph/properties.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Validate
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["graph/validate.jl"]
|
||||
Order = [:function]
|
||||
```
|
28
docs/src/lib/internals/models.md
Normal file
28
docs/src/lib/internals/models.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Models
|
||||
|
||||
## ABC-Model
|
||||
|
||||
### Types
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["models/abc/types.jl"]
|
||||
Order = [:type, :constant]
|
||||
```
|
||||
|
||||
### Parse
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["models/abc/parse.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
### Properties
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["models/abc/properties.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## QED-Model
|
||||
|
||||
*To be added*
|
43
docs/src/lib/internals/node.md
Normal file
43
docs/src/lib/internals/node.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Node
|
||||
|
||||
## Type
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["node/type.jl"]
|
||||
Order = [:type]
|
||||
```
|
||||
|
||||
## Create
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["node/create.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Compare
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["node/compare.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Properties
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["node/properties.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Print
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["node/print.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Validate
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["node/validate.jl"]
|
||||
Order = [:function]
|
||||
```
|
57
docs/src/lib/internals/operation.md
Normal file
57
docs/src/lib/internals/operation.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Operation
|
||||
|
||||
## Types
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["operation/type.jl"]
|
||||
Order = [:type]
|
||||
```
|
||||
|
||||
## Find
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["operation/find.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Apply
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["operation/apply.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Get
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["operation/get.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Clean
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["operation/clean.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Utility
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["operation/utility.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Print
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["operation/print.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Validate
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["operation/validate.jl"]
|
||||
Order = [:function]
|
||||
```
|
36
docs/src/lib/internals/task.md
Normal file
36
docs/src/lib/internals/task.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Task
|
||||
|
||||
## Type
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["task/type.jl"]
|
||||
Order = [:type]
|
||||
```
|
||||
|
||||
## Create
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["task/create.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Compare
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["task/compare.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Properties
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["task/properties.jl"]
|
||||
Order = [:function]
|
||||
```
|
||||
|
||||
## Print
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["task/print.jl"]
|
||||
Order = [:function]
|
||||
```
|
17
docs/src/lib/internals/utility.md
Normal file
17
docs/src/lib/internals/utility.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Utility
|
||||
|
||||
## Helper Functions
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["utility.jl"]
|
||||
Order = [:type, :function]
|
||||
```
|
||||
|
||||
## Trie Helper
|
||||
This is a simple implementation of a [Trie Data Structure](https://en.wikipedia.org/wiki/Trie) to greatly improve the performance of the Node Reduction search.
|
||||
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["trie.jl"]
|
||||
Order = [:type, :function]
|
||||
```
|
24
docs/src/lib/public.md
Normal file
24
docs/src/lib/public.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Public Documentation
|
||||
|
||||
Documentation for `MetagraphOptimization.jl`'s public interface.
|
||||
|
||||
See the Internals section of the manual for documentation of everything else.
|
||||
|
||||
```@autodocs
|
||||
Modules = [MetagraphOptimization]
|
||||
Pages = ["MetagraphOptimization.jl"]
|
||||
Order = [:module]
|
||||
```
|
||||
|
||||
## Contents
|
||||
|
||||
```@contents
|
||||
Pages = ["public.md"]
|
||||
Depth = 2
|
||||
```
|
||||
|
||||
## Index
|
||||
|
||||
```@index
|
||||
Pages = ["public.md"]
|
||||
```
|
3
docs/src/manual.md
Normal file
3
docs/src/manual.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Manual
|
||||
|
||||
This will become a manual.
|
@ -1,7 +1,3 @@
|
||||
[deps]
|
||||
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
|
||||
MetagraphOptimization = "3e869610-d48d-4942-ba70-c1b702a33ca4"
|
||||
PProf = "e4faabce-9ead-11e9-39d9-4379958e3056"
|
||||
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
|
||||
ProfileView = "c46f51b8-102a-5cf2-8d2c-8597cb0e0da7"
|
||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
|
@ -16,7 +16,10 @@ function bench_txt(filepath::String, bench::Bool = true)
|
||||
println(name, ":")
|
||||
g = parse_abc(filepath)
|
||||
print(g)
|
||||
#println(" Graph size in memory: ", bytes_to_human_readable(Base.summarysize(g)))
|
||||
println(
|
||||
" Graph size in memory: ",
|
||||
bytes_to_human_readable(MetagraphOptimization.mem(g)),
|
||||
)
|
||||
|
||||
if (bench)
|
||||
@btime parse_abc($filepath)
|
||||
@ -24,7 +27,7 @@ function bench_txt(filepath::String, bench::Bool = true)
|
||||
|
||||
println(" Get Operations: ")
|
||||
@time get_operations(g)
|
||||
println()
|
||||
return println()
|
||||
end
|
||||
|
||||
function import_bench()
|
||||
@ -34,7 +37,7 @@ function import_bench()
|
||||
bench_txt("AB->ABBBBBBB.txt")
|
||||
#bench_txt("AB->ABBBBBBBBB.txt")
|
||||
bench_txt("ABAB->ABAB.txt")
|
||||
bench_txt("ABAB->ABC.txt")
|
||||
return bench_txt("ABAB->ABC.txt")
|
||||
end
|
||||
|
||||
import_bench()
|
||||
|
@ -21,7 +21,7 @@ function gen_plot(filepath)
|
||||
x = Vector{Float64}()
|
||||
y = Vector{Float64}()
|
||||
|
||||
for i = 1:30
|
||||
for i in 1:30
|
||||
print("\r", i)
|
||||
# push
|
||||
opt = get_operations(g)
|
||||
@ -38,7 +38,7 @@ function gen_plot(filepath)
|
||||
push_operation!(g, rand(collect(opt.nodeSplits)))
|
||||
println("NS")
|
||||
else
|
||||
i = i-1
|
||||
i = i - 1
|
||||
end
|
||||
|
||||
props = graph_properties(g)
|
||||
@ -48,13 +48,26 @@ function gen_plot(filepath)
|
||||
|
||||
println("\rDone.")
|
||||
|
||||
plot([x[1], x[2]], [y[1], y[2]], linestyle = :solid, linewidth = 1, color = :red, legend=false)
|
||||
plot(
|
||||
[x[1], x[2]],
|
||||
[y[1], y[2]],
|
||||
linestyle = :solid,
|
||||
linewidth = 1,
|
||||
color = :red,
|
||||
legend = false,
|
||||
)
|
||||
# Create lines connecting the reference point to each data point
|
||||
for i in 3:length(x)
|
||||
plot!([x[i-1], x[i]], [y[i-1], y[i]], linestyle = :solid, linewidth = 1, color = :red)
|
||||
plot!(
|
||||
[x[i - 1], x[i]],
|
||||
[y[i - 1], y[i]],
|
||||
linestyle = :solid,
|
||||
linewidth = 1,
|
||||
color = :red,
|
||||
)
|
||||
end
|
||||
|
||||
gui()
|
||||
return gui()
|
||||
end
|
||||
|
||||
gen_plot("AB->ABBB.txt")
|
||||
|
@ -18,7 +18,7 @@ function gen_plot(filepath)
|
||||
|
||||
println("Random Walking... ")
|
||||
|
||||
for i = 1:30
|
||||
for i in 1:30
|
||||
print("\r", i)
|
||||
# push
|
||||
opt = get_operations(g)
|
||||
@ -35,7 +35,7 @@ function gen_plot(filepath)
|
||||
push_operation!(g, rand(collect(opt.nodeSplits)))
|
||||
println("NS")
|
||||
else
|
||||
i = i-1
|
||||
i = i - 1
|
||||
end
|
||||
end
|
||||
|
||||
@ -60,7 +60,14 @@ function gen_plot(filepath)
|
||||
push!(y, props.compute_effort)
|
||||
pop_operation!(g)
|
||||
|
||||
push!(names, "NF: (" * string(props.data) * ", " * string(props.compute_effort) * ")")
|
||||
push!(
|
||||
names,
|
||||
"NF: (" *
|
||||
string(props.data) *
|
||||
", " *
|
||||
string(props.compute_effort) *
|
||||
")",
|
||||
)
|
||||
end
|
||||
for op in opt.nodeReductions
|
||||
push_operation!(g, op)
|
||||
@ -69,7 +76,14 @@ function gen_plot(filepath)
|
||||
push!(y, props.compute_effort)
|
||||
pop_operation!(g)
|
||||
|
||||
push!(names, "NR: (" * string(props.data) * ", " * string(props.compute_effort) * ")")
|
||||
push!(
|
||||
names,
|
||||
"NR: (" *
|
||||
string(props.data) *
|
||||
", " *
|
||||
string(props.compute_effort) *
|
||||
")",
|
||||
)
|
||||
end
|
||||
for op in opt.nodeSplits
|
||||
push_operation!(g, op)
|
||||
@ -78,19 +92,39 @@ function gen_plot(filepath)
|
||||
push!(y, props.compute_effort)
|
||||
pop_operation!(g)
|
||||
|
||||
push!(names, "NS: (" * string(props.data) * ", " * string(props.compute_effort) * ")")
|
||||
push!(
|
||||
names,
|
||||
"NS: (" *
|
||||
string(props.data) *
|
||||
", " *
|
||||
string(props.compute_effort) *
|
||||
")",
|
||||
)
|
||||
end
|
||||
|
||||
plot([x0, x[1]], [y0, y[1]], linestyle = :solid, linewidth = 1, color = :red, legend=false)
|
||||
|
||||
plot(
|
||||
[x0, x[1]],
|
||||
[y0, y[1]],
|
||||
linestyle = :solid,
|
||||
linewidth = 1,
|
||||
color = :red,
|
||||
legend = false,
|
||||
)
|
||||
# Create lines connecting the reference point to each data point
|
||||
for i in 2:length(x)
|
||||
plot!([x0, x[i]], [y0, y[i]], linestyle = :solid, linewidth = 1, color = :red)
|
||||
plot!(
|
||||
[x0, x[i]],
|
||||
[y0, y[i]],
|
||||
linestyle = :solid,
|
||||
linewidth = 1,
|
||||
color = :red,
|
||||
)
|
||||
end
|
||||
#scatter!(x, y, label=names)
|
||||
|
||||
print(names)
|
||||
|
||||
gui()
|
||||
return gui()
|
||||
end
|
||||
|
||||
gen_plot("AB->ABBB.txt")
|
||||
|
@ -5,7 +5,7 @@ function test_random_walk(g::DAG, n::Int64)
|
||||
|
||||
properties = graph_properties(g)
|
||||
|
||||
for i = 1:n
|
||||
for i in 1:n
|
||||
# choose push or pop
|
||||
if rand(Bool)
|
||||
# push
|
||||
@ -32,5 +32,5 @@ function test_random_walk(g::DAG, n::Int64)
|
||||
end
|
||||
end
|
||||
|
||||
reset_graph!(g)
|
||||
end
|
||||
return reset_graph!(g)
|
||||
end
|
||||
|
@ -1,10 +1,52 @@
|
||||
"""
|
||||
MetagraphOptimization
|
||||
|
||||
A module containing tools to work on DAGs.
|
||||
"""
|
||||
module MetagraphOptimization
|
||||
|
||||
export Node, Edge, ComputeTaskNode, DataTaskNode, DAG
|
||||
export AbstractTask, AbstractComputeTask, AbstractDataTask, DataTask, FusedComputeTask
|
||||
export make_node, make_edge, insert_node, insert_edge, is_entry_node, is_exit_node, parents, children, compute, graph_properties, get_exit_node, is_valid
|
||||
export NodeFusion, NodeReduction, NodeSplit, push_operation!, pop_operation!, can_pop, reset_graph!, get_operations
|
||||
export parse_abc, ComputeTaskP, ComputeTaskS1, ComputeTaskS2, ComputeTaskV, ComputeTaskU, ComputeTaskSum
|
||||
export DAG
|
||||
export Node
|
||||
export Edge
|
||||
export ComputeTaskNode
|
||||
export DataTaskNode
|
||||
export AbstractTask
|
||||
export AbstractComputeTask
|
||||
export AbstractDataTask
|
||||
export DataTask
|
||||
export FusedComputeTask
|
||||
|
||||
export make_node
|
||||
export make_edge
|
||||
export insert_node
|
||||
export insert_edge
|
||||
export is_entry_node
|
||||
export is_exit_node
|
||||
export parents
|
||||
export children
|
||||
export compute
|
||||
export graph_properties
|
||||
export get_exit_node
|
||||
export is_valid
|
||||
|
||||
export Operation
|
||||
export AppliedOperation
|
||||
export NodeFusion
|
||||
export NodeReduction
|
||||
export NodeSplit
|
||||
export push_operation!
|
||||
export pop_operation!
|
||||
export can_pop
|
||||
export reset_graph!
|
||||
export get_operations
|
||||
|
||||
export parse_abc
|
||||
export ComputeTaskP
|
||||
export ComputeTaskS1
|
||||
export ComputeTaskS2
|
||||
export ComputeTaskV
|
||||
export ComputeTaskU
|
||||
export ComputeTaskSum
|
||||
|
||||
export ==, in, show, isempty, delete!, length
|
||||
|
||||
@ -21,29 +63,46 @@ import Base.insert!
|
||||
import Base.collect
|
||||
|
||||
|
||||
include("tasks.jl")
|
||||
include("nodes.jl")
|
||||
include("graph.jl")
|
||||
include("task/type.jl")
|
||||
include("node/type.jl")
|
||||
include("diff/type.jl")
|
||||
include("operation/type.jl")
|
||||
include("graph/type.jl")
|
||||
|
||||
include("trie.jl")
|
||||
include("utility.jl")
|
||||
|
||||
include("task_functions.jl")
|
||||
include("node_functions.jl")
|
||||
include("graph_functions.jl")
|
||||
include("diff/print.jl")
|
||||
include("diff/properties.jl")
|
||||
|
||||
include("operations/utility.jl")
|
||||
include("operations/apply.jl")
|
||||
include("operations/clean.jl")
|
||||
include("operations/find.jl")
|
||||
include("operations/get.jl")
|
||||
include("operations/print.jl")
|
||||
include("operations/validate.jl")
|
||||
include("graph/compare.jl")
|
||||
include("graph/interface.jl")
|
||||
include("graph/mute.jl")
|
||||
include("graph/print.jl")
|
||||
include("graph/properties.jl")
|
||||
include("graph/validate.jl")
|
||||
|
||||
include("graph_interface.jl")
|
||||
include("node/compare.jl")
|
||||
include("node/create.jl")
|
||||
include("node/print.jl")
|
||||
include("node/properties.jl")
|
||||
include("node/validate.jl")
|
||||
|
||||
include("abc_model/tasks.jl")
|
||||
include("abc_model/task_functions.jl")
|
||||
include("abc_model/parse.jl")
|
||||
include("operation/utility.jl")
|
||||
include("operation/apply.jl")
|
||||
include("operation/clean.jl")
|
||||
include("operation/find.jl")
|
||||
include("operation/get.jl")
|
||||
include("operation/print.jl")
|
||||
include("operation/validate.jl")
|
||||
|
||||
include("task/create.jl")
|
||||
include("task/compare.jl")
|
||||
include("task/print.jl")
|
||||
include("task/properties.jl")
|
||||
|
||||
include("models/abc/types.jl")
|
||||
include("models/abc/properties.jl")
|
||||
include("models/abc/parse.jl")
|
||||
|
||||
end # module MetagraphOptimization
|
||||
|
@ -1,21 +0,0 @@
|
||||
# define compute_efforts tasks computation
|
||||
# put some "random" numbers here for now
|
||||
compute_effort(t::ComputeTaskS1) = 10
|
||||
compute_effort(t::ComputeTaskS2) = 10
|
||||
compute_effort(t::ComputeTaskU) = 6
|
||||
compute_effort(t::ComputeTaskV) = 20
|
||||
compute_effort(t::ComputeTaskP) = 15
|
||||
compute_effort(t::ComputeTaskSum) = 1
|
||||
|
||||
function show(io::IO, t::DataTask)
|
||||
print(io, "Data", t.data)
|
||||
end
|
||||
|
||||
show(io::IO, t::ComputeTaskS1) = print("ComputeS1")
|
||||
show(io::IO, t::ComputeTaskS2) = print("ComputeS2")
|
||||
show(io::IO, t::ComputeTaskP) = print("ComputeP")
|
||||
show(io::IO, t::ComputeTaskU) = print("ComputeU")
|
||||
show(io::IO, t::ComputeTaskV) = print("ComputeV")
|
||||
show(io::IO, t::ComputeTaskSum) = print("ComputeSum")
|
||||
|
||||
copy(t::DataTask) = DataTask(t.data)
|
@ -1,29 +0,0 @@
|
||||
struct DataTask <: AbstractDataTask
|
||||
data::UInt64
|
||||
end
|
||||
|
||||
# S task with 1 child
|
||||
struct ComputeTaskS1 <: AbstractComputeTask
|
||||
end
|
||||
|
||||
# S task with 2 children
|
||||
struct ComputeTaskS2 <: AbstractComputeTask
|
||||
end
|
||||
|
||||
# P task with 0 children
|
||||
struct ComputeTaskP <: AbstractComputeTask
|
||||
end
|
||||
|
||||
# v task with 2 children
|
||||
struct ComputeTaskV <: AbstractComputeTask
|
||||
end
|
||||
|
||||
# u task with 1 child
|
||||
struct ComputeTaskU <: AbstractComputeTask
|
||||
end
|
||||
|
||||
# task that sums all its inputs, n children
|
||||
struct ComputeTaskSum <: AbstractComputeTask
|
||||
end
|
||||
|
||||
ABC_TASKS = [DataTask, ComputeTaskS1, ComputeTaskS2, ComputeTaskP, ComputeTaskV, ComputeTaskU, ComputeTaskSum]
|
11
src/diff/print.jl
Normal file
11
src/diff/print.jl
Normal file
@ -0,0 +1,11 @@
|
||||
"""
|
||||
show(io::IO, diff::Diff)
|
||||
|
||||
Pretty-print a [`Diff`](@ref). Called via print, println and co.
|
||||
"""
|
||||
function show(io::IO, diff::Diff)
|
||||
print(io, "Nodes: ")
|
||||
print(io, length(diff.addedNodes) + length(diff.removedNodes))
|
||||
print(io, " Edges: ")
|
||||
return print(io, length(diff.addedEdges) + length(diff.removedEdges))
|
||||
end
|
14
src/diff/properties.jl
Normal file
14
src/diff/properties.jl
Normal file
@ -0,0 +1,14 @@
|
||||
"""
|
||||
length(diff::Diff)
|
||||
|
||||
Return a named tuple of the lengths of the added/removed nodes/edges.
|
||||
The fields are `.addedNodes`, `.addedEdges`, `.removedNodes` and `.removedEdges`.
|
||||
"""
|
||||
function length(diff::Diff)
|
||||
return (
|
||||
addedNodes = length(diff.addedNodes),
|
||||
removedNodes = length(diff.removedNodes),
|
||||
addedEdges = length(diff.addedEdges),
|
||||
removedEdges = length(diff.removedEdges),
|
||||
)
|
||||
end
|
18
src/diff/type.jl
Normal file
18
src/diff/type.jl
Normal file
@ -0,0 +1,18 @@
|
||||
"""
|
||||
Diff
|
||||
|
||||
A named tuple representing a difference of added and removed nodes and edges on a [`DAG`](@ref).
|
||||
"""
|
||||
const Diff = NamedTuple{
|
||||
(:addedNodes, :removedNodes, :addedEdges, :removedEdges),
|
||||
Tuple{Vector{Node}, Vector{Node}, Vector{Edge}, Vector{Edge}},
|
||||
}
|
||||
|
||||
function Diff()
|
||||
return (
|
||||
addedNodes = Vector{Node}(),
|
||||
removedNodes = Vector{Node}(),
|
||||
addedEdges = Vector{Edge}(),
|
||||
removedEdges = Vector{Edge}(),
|
||||
)::Diff
|
||||
end
|
90
src/graph.jl
90
src/graph.jl
@ -1,90 +0,0 @@
|
||||
using DataStructures
|
||||
|
||||
const Diff = NamedTuple{
|
||||
(:addedNodes, :removedNodes, :addedEdges, :removedEdges),
|
||||
Tuple{Vector{Node}, Vector{Node}, Vector{Edge}, Vector{Edge}}
|
||||
}
|
||||
|
||||
function Diff()
|
||||
return (
|
||||
addedNodes = Vector{Node}(),
|
||||
removedNodes = Vector{Node}(),
|
||||
addedEdges = Vector{Edge}(),
|
||||
removedEdges = Vector{Edge}()
|
||||
)::Diff
|
||||
end
|
||||
|
||||
# An abstract base class for operations
|
||||
# an operation can be applied to a DAG
|
||||
abstract type Operation end
|
||||
|
||||
# An abstract base class for already applied operations
|
||||
# an applied operation can be reversed iff it is the last applied operation on the DAG
|
||||
abstract type AppliedOperation end
|
||||
|
||||
struct NodeFusion <: Operation
|
||||
input::Tuple{ComputeTaskNode, DataTaskNode, ComputeTaskNode}
|
||||
end
|
||||
|
||||
struct AppliedNodeFusion <: AppliedOperation
|
||||
operation::NodeFusion
|
||||
diff::Diff
|
||||
end
|
||||
|
||||
struct NodeReduction <: Operation
|
||||
input::Vector{Node}
|
||||
end
|
||||
|
||||
struct AppliedNodeReduction <: AppliedOperation
|
||||
operation::NodeReduction
|
||||
diff::Diff
|
||||
end
|
||||
|
||||
struct NodeSplit <: Operation
|
||||
input::Node
|
||||
end
|
||||
|
||||
struct AppliedNodeSplit <: AppliedOperation
|
||||
operation::NodeSplit
|
||||
diff::Diff
|
||||
end
|
||||
|
||||
mutable struct PossibleOperations
|
||||
nodeFusions::Set{NodeFusion}
|
||||
nodeReductions::Set{NodeReduction}
|
||||
nodeSplits::Set{NodeSplit}
|
||||
end
|
||||
|
||||
function PossibleOperations()
|
||||
return PossibleOperations(
|
||||
Set{NodeFusion}(),
|
||||
Set{NodeReduction}(),
|
||||
Set{NodeSplit}()
|
||||
)
|
||||
end
|
||||
|
||||
# The actual state of the DAG is the initial state given by the set of nodes
|
||||
# but with all the operations in appliedChain applied in order
|
||||
mutable struct DAG
|
||||
nodes::Set{Node}
|
||||
|
||||
# The operations currently applied to the set of nodes
|
||||
appliedOperations::Stack{AppliedOperation}
|
||||
|
||||
# The operations not currently applied but part of the current state of the DAG
|
||||
operationsToApply::Deque{Operation}
|
||||
|
||||
# The possible operations at the current state of the DAG
|
||||
possibleOperations::PossibleOperations
|
||||
|
||||
# The set of nodes whose possible operations need to be reevaluated
|
||||
dirtyNodes::Set{Node}
|
||||
|
||||
# "snapshot" system: keep track of added/removed nodes/edges since last snapshot
|
||||
# these are muted in insert_node! etc.
|
||||
diff::Diff
|
||||
end
|
||||
|
||||
function DAG()
|
||||
return DAG(Set{Node}(), Stack{AppliedOperation}(), Deque{Operation}(), PossibleOperations(), Set{Node}(), Diff())
|
||||
end
|
37
src/graph/compare.jl
Normal file
37
src/graph/compare.jl
Normal file
@ -0,0 +1,37 @@
|
||||
"""
|
||||
in(node::Node, graph::DAG)
|
||||
|
||||
Check whether the node is part of the graph.
|
||||
"""
|
||||
in(node::Node, graph::DAG) = node in graph.nodes
|
||||
|
||||
"""
|
||||
in(edge::Edge, graph::DAG)
|
||||
|
||||
Check whether the edge is part of the graph.
|
||||
"""
|
||||
function in(edge::Edge, graph::DAG)
|
||||
n1 = edge.edge[1]
|
||||
n2 = edge.edge[2]
|
||||
if !(n1 in graph) || !(n2 in graph)
|
||||
return false
|
||||
end
|
||||
|
||||
return n1 in n2.children
|
||||
end
|
||||
|
||||
"""
|
||||
==(n1::Node, n2::Node, g::DAG)
|
||||
|
||||
Check equality of two nodes in a graph.
|
||||
"""
|
||||
function ==(n1::Node, n2::Node, g::DAG)
|
||||
if typeof(n1) != typeof(n2)
|
||||
return false
|
||||
end
|
||||
if !(n1 in g) || !(n2 in g)
|
||||
return false
|
||||
end
|
||||
|
||||
return n1.task == n2.task && children(n1) == children(n2)
|
||||
end
|
55
src/graph/interface.jl
Normal file
55
src/graph/interface.jl
Normal file
@ -0,0 +1,55 @@
|
||||
"""
|
||||
push_operation!(graph::DAG, operation::Operation)
|
||||
|
||||
Apply a new operation to the graph.
|
||||
|
||||
See also: [`DAG`](@ref), [`pop_operation!`](@ref)
|
||||
"""
|
||||
function push_operation!(graph::DAG, operation::Operation)
|
||||
# 1.: Add the operation to the DAG
|
||||
push!(graph.operationsToApply, operation)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
pop_operation!(graph::DAG)
|
||||
|
||||
Revert the latest applied operation on the graph.
|
||||
|
||||
See also: [`DAG`](@ref), [`push_operation!`](@ref)
|
||||
"""
|
||||
function pop_operation!(graph::DAG)
|
||||
# 1.: Remove the operation from the appliedChain of the DAG
|
||||
if !isempty(graph.operationsToApply)
|
||||
pop!(graph.operationsToApply)
|
||||
elseif !isempty(graph.appliedOperations)
|
||||
appliedOp = pop!(graph.appliedOperations)
|
||||
revert_operation!(graph, appliedOp)
|
||||
else
|
||||
error("No more operations to pop!")
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
can_pop(graph::DAG)
|
||||
|
||||
Return `true` if [`pop_operation!`](@ref) is possible, `false` otherwise.
|
||||
"""
|
||||
can_pop(graph::DAG) =
|
||||
!isempty(graph.operationsToApply) || !isempty(graph.appliedOperations)
|
||||
|
||||
"""
|
||||
reset_graph!(graph::DAG)
|
||||
|
||||
Reset the graph to its initial state with no operations applied.
|
||||
"""
|
||||
function reset_graph!(graph::DAG)
|
||||
while (can_pop(graph))
|
||||
pop_operation!(graph)
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
280
src/graph/mute.jl
Normal file
280
src/graph/mute.jl
Normal file
@ -0,0 +1,280 @@
|
||||
# for graph mutating functions we need to do a few things
|
||||
# 1: mute the graph (duh)
|
||||
# 2: keep track of what was changed for the diff (if track == true)
|
||||
# 3: invalidate operation caches
|
||||
|
||||
"""
|
||||
insert_node!(graph::DAG, node::Node; track = true, invalidate_cache = true)
|
||||
|
||||
Insert the node into the graph.
|
||||
|
||||
## Keyword Arguments
|
||||
`track::Bool`: Whether to add the changes to the [`DAG`](@ref)'s [`Diff`](@ref). Should be set `false` in parsing or graph creation functions for performance.
|
||||
|
||||
`invalidate_cache::Bool`: Whether to invalidate caches associated with the changes. Should also be turned off for graph creation or parsing.
|
||||
|
||||
See also: [`remove_node!`](@ref), [`insert_edge!`](@ref), [`remove_edge!`](@ref)
|
||||
"""
|
||||
function insert_node!(
|
||||
graph::DAG,
|
||||
node::Node,
|
||||
track = true,
|
||||
invalidate_cache = true,
|
||||
)
|
||||
# 1: mute
|
||||
push!(graph.nodes, node)
|
||||
|
||||
# 2: keep track
|
||||
if (track)
|
||||
push!(graph.diff.addedNodes, node)
|
||||
end
|
||||
|
||||
# 3: invalidate caches
|
||||
if (!invalidate_cache)
|
||||
return node
|
||||
end
|
||||
push!(graph.dirtyNodes, node)
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
"""
|
||||
insert_edge!(graph::DAG, node1::Node, node2::Node; track = true, invalidate_cache = true)
|
||||
|
||||
Insert the edge between node1 (child) and node2 (parent) into the graph.
|
||||
|
||||
## Keyword Arguments
|
||||
`track::Bool`: Whether to add the changes to the [`DAG`](@ref)'s [`Diff`](@ref). Should be set `false` in parsing or graph creation functions for performance.
|
||||
|
||||
`invalidate_cache::Bool`: Whether to invalidate caches associated with the changes. Should also be turned off for graph creation or parsing.
|
||||
|
||||
See also: [`insert_node!`](@ref), [`remove_node!`](@ref), [`remove_edge!`](@ref)
|
||||
"""
|
||||
function insert_edge!(
|
||||
graph::DAG,
|
||||
node1::Node,
|
||||
node2::Node,
|
||||
track = true,
|
||||
invalidate_cache = true,
|
||||
)
|
||||
# @assert (node2 ∉ node1.parents) && (node1 ∉ node2.children) "Edge to insert already exists"
|
||||
|
||||
# 1: mute
|
||||
# edge points from child to parent
|
||||
push!(node1.parents, node2)
|
||||
push!(node2.children, node1)
|
||||
|
||||
# 2: keep track
|
||||
if (track)
|
||||
push!(graph.diff.addedEdges, make_edge(node1, node2))
|
||||
end
|
||||
|
||||
# 3: invalidate caches
|
||||
if (!invalidate_cache)
|
||||
return nothing
|
||||
end
|
||||
|
||||
invalidate_operation_caches!(graph, node1)
|
||||
invalidate_operation_caches!(graph, node2)
|
||||
|
||||
push!(graph.dirtyNodes, node1)
|
||||
push!(graph.dirtyNodes, node2)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
remove_node!(graph::DAG, node::Node; track = true, invalidate_cache = true)
|
||||
|
||||
Remove the node from the graph.
|
||||
|
||||
## Keyword Arguments
|
||||
`track::Bool`: Whether to add the changes to the [`DAG`](@ref)'s [`Diff`](@ref). Should be set `false` in parsing or graph creation functions for performance.
|
||||
|
||||
`invalidate_cache::Bool`: Whether to invalidate caches associated with the changes. Should also be turned off for graph creation or parsing.
|
||||
|
||||
See also: [`insert_node!`](@ref), [`insert_edge!`](@ref), [`remove_edge!`](@ref)
|
||||
"""
|
||||
function remove_node!(
|
||||
graph::DAG,
|
||||
node::Node,
|
||||
track = true,
|
||||
invalidate_cache = true,
|
||||
)
|
||||
# @assert node in graph.nodes "Trying to remove a node that's not in the graph"
|
||||
|
||||
# 1: mute
|
||||
delete!(graph.nodes, node)
|
||||
|
||||
# 2: keep track
|
||||
if (track)
|
||||
push!(graph.diff.removedNodes, node)
|
||||
end
|
||||
|
||||
# 3: invalidate caches
|
||||
if (!invalidate_cache)
|
||||
return nothing
|
||||
end
|
||||
|
||||
invalidate_operation_caches!(graph, node)
|
||||
delete!(graph.dirtyNodes, node)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
remove_edge!(graph::DAG, node1::Node, node2::Node; track = true, invalidate_cache = true)
|
||||
|
||||
Remove the edge between node1 (child) and node2 (parent) into the graph.
|
||||
|
||||
## Keyword Arguments
|
||||
`track::Bool`: Whether to add the changes to the [`DAG`](@ref)'s [`Diff`](@ref). Should be set `false` in parsing or graph creation functions for performance.
|
||||
|
||||
`invalidate_cache::Bool`: Whether to invalidate caches associated with the changes. Should also be turned off for graph creation or parsing.
|
||||
|
||||
See also: [`insert_node!`](@ref), [`remove_node!`](@ref), [`insert_edge!`](@ref)
|
||||
"""
|
||||
function remove_edge!(
|
||||
graph::DAG,
|
||||
node1::Node,
|
||||
node2::Node,
|
||||
track = true,
|
||||
invalidate_cache = true,
|
||||
)
|
||||
# 1: mute
|
||||
pre_length1 = length(node1.parents)
|
||||
pre_length2 = length(node2.children)
|
||||
filter!(x -> x != node2, node1.parents)
|
||||
filter!(x -> x != node1, node2.children)
|
||||
|
||||
#=@assert begin
|
||||
removed = pre_length1 - length(node1.parents)
|
||||
removed <= 1
|
||||
end "removed more than one node from node1's parents"=#
|
||||
|
||||
#=@assert begin
|
||||
removed = pre_length2 - length(node2.children)
|
||||
removed <= 1
|
||||
end "removed more than one node from node2's children"=#
|
||||
|
||||
# 2: keep track
|
||||
if (track)
|
||||
push!(graph.diff.removedEdges, make_edge(node1, node2))
|
||||
end
|
||||
|
||||
# 3: invalidate caches
|
||||
if (!invalidate_cache)
|
||||
return nothing
|
||||
end
|
||||
|
||||
invalidate_operation_caches!(graph, node1)
|
||||
invalidate_operation_caches!(graph, node2)
|
||||
if (node1 in graph)
|
||||
push!(graph.dirtyNodes, node1)
|
||||
end
|
||||
if (node2 in graph)
|
||||
push!(graph.dirtyNodes, node2)
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
get_snapshot_diff(graph::DAG)
|
||||
|
||||
Return the graph's [`Diff`](@ref) since last time this function was called.
|
||||
|
||||
See also: [`revert_diff!`](@ref), [`AppliedOperation`](@ref) and [`revert_operation!`](@ref)
|
||||
"""
|
||||
function get_snapshot_diff(graph::DAG)
|
||||
return swapfield!(graph, :diff, Diff())
|
||||
end
|
||||
|
||||
"""
|
||||
invalidate_caches!(graph::DAG, operation::NodeFusion)
|
||||
|
||||
Invalidate the operation caches for a given [`NodeFusion`](@ref).
|
||||
|
||||
This deletes the operation from the graph's possible operations and from the involved nodes' own operation caches.
|
||||
"""
|
||||
function invalidate_caches!(graph::DAG, operation::NodeFusion)
|
||||
delete!(graph.possibleOperations, operation)
|
||||
|
||||
# delete the operation from all caches of nodes involved in the operation
|
||||
filter!(!=(operation), operation.input[1].nodeFusions)
|
||||
filter!(!=(operation), operation.input[3].nodeFusions)
|
||||
|
||||
operation.input[2].nodeFusion = missing
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
invalidate_caches!(graph::DAG, operation::NodeReduction)
|
||||
|
||||
Invalidate the operation caches for a given [`NodeReduction`](@ref).
|
||||
|
||||
This deletes the operation from the graph's possible operations and from the involved nodes' own operation caches.
|
||||
"""
|
||||
function invalidate_caches!(graph::DAG, operation::NodeReduction)
|
||||
delete!(graph.possibleOperations, operation)
|
||||
|
||||
for node in operation.input
|
||||
node.nodeReduction = missing
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
invalidate_caches!(graph::DAG, operation::NodeSplit)
|
||||
|
||||
Invalidate the operation caches for a given [`NodeSplit`](@ref).
|
||||
|
||||
This deletes the operation from the graph's possible operations and from the involved nodes' own operation caches.
|
||||
"""
|
||||
function invalidate_caches!(graph::DAG, operation::NodeSplit)
|
||||
delete!(graph.possibleOperations, operation)
|
||||
|
||||
# delete the operation from all caches of nodes involved in the operation
|
||||
# for node split there is only one node
|
||||
operation.input.nodeSplit = missing
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
invalidate_operation_caches!(graph::DAG, node::ComputeTaskNode)
|
||||
|
||||
Invalidate the operation caches of the given node through calls to the respective [`invalidate_caches!`](@ref) functions.
|
||||
"""
|
||||
function invalidate_operation_caches!(graph::DAG, node::ComputeTaskNode)
|
||||
if !ismissing(node.nodeReduction)
|
||||
invalidate_caches!(graph, node.nodeReduction)
|
||||
end
|
||||
if !ismissing(node.nodeSplit)
|
||||
invalidate_caches!(graph, node.nodeSplit)
|
||||
end
|
||||
while !isempty(node.nodeFusions)
|
||||
invalidate_caches!(graph, pop!(node.nodeFusions))
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
invalidate_operation_caches!(graph::DAG, node::DataTaskNode)
|
||||
|
||||
Invalidate the operation caches of the given node through calls to the respective [`invalidate_caches!`](@ref) functions.
|
||||
"""
|
||||
function invalidate_operation_caches!(graph::DAG, node::DataTaskNode)
|
||||
if !ismissing(node.nodeReduction)
|
||||
invalidate_caches!(graph, node.nodeReduction)
|
||||
end
|
||||
if !ismissing(node.nodeSplit)
|
||||
invalidate_caches!(graph, node.nodeSplit)
|
||||
end
|
||||
if !ismissing(node.nodeFusion)
|
||||
invalidate_caches!(graph, node.nodeFusion)
|
||||
end
|
||||
return nothing
|
||||
end
|
69
src/graph/print.jl
Normal file
69
src/graph/print.jl
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
show_nodes(io::IO, graph::DAG)
|
||||
|
||||
Print a graph's nodes. Should only be used for small graphs as it prints every node in a list.
|
||||
"""
|
||||
function show_nodes(io::IO, graph::DAG)
|
||||
print(io, "[")
|
||||
first = true
|
||||
for n in graph.nodes
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
print(io, ", ")
|
||||
end
|
||||
print(io, n)
|
||||
end
|
||||
return print(io, "]")
|
||||
end
|
||||
|
||||
"""
|
||||
show(io::IO, graph::DAG)
|
||||
|
||||
Print the given graph to io. If there are too many nodes it will print only a summary of them.
|
||||
"""
|
||||
function show(io::IO, graph::DAG)
|
||||
println(io, "Graph:")
|
||||
print(io, " Nodes: ")
|
||||
|
||||
nodeDict = Dict{Type, Int64}()
|
||||
noEdges = 0
|
||||
for node in graph.nodes
|
||||
if haskey(nodeDict, typeof(node.task))
|
||||
nodeDict[typeof(node.task)] = nodeDict[typeof(node.task)] + 1
|
||||
else
|
||||
nodeDict[typeof(node.task)] = 1
|
||||
end
|
||||
noEdges += length(parents(node))
|
||||
end
|
||||
|
||||
if length(graph.nodes) <= 20
|
||||
show_nodes(io, graph)
|
||||
else
|
||||
print("Total: ", length(graph.nodes), ", ")
|
||||
first = true
|
||||
i = 0
|
||||
for (type, number) in zip(keys(nodeDict), values(nodeDict))
|
||||
i += 1
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
print(", ")
|
||||
end
|
||||
if (i % 3 == 0)
|
||||
print("\n ")
|
||||
end
|
||||
print(type, ": ", number)
|
||||
end
|
||||
end
|
||||
println(io)
|
||||
println(io, " Edges: ", noEdges)
|
||||
properties = graph_properties(graph)
|
||||
println(io, " Total Compute Effort: ", properties.compute_effort)
|
||||
println(io, " Total Data Transfer: ", properties.data)
|
||||
return println(
|
||||
io,
|
||||
" Total Compute Intensity: ",
|
||||
properties.compute_intensity,
|
||||
)
|
||||
end
|
43
src/graph/properties.jl
Normal file
43
src/graph/properties.jl
Normal file
@ -0,0 +1,43 @@
|
||||
"""
|
||||
graph_properties(graph::DAG)
|
||||
|
||||
Return the graph's properties, a named tuple with fields `.data`, `.compute_effort`, `.compute_intensity`, `.nodes` (number of nodes) and `.edges` (number of edges).
|
||||
"""
|
||||
function graph_properties(graph::DAG)
|
||||
# make sure the graph is fully generated
|
||||
apply_all!(graph)
|
||||
|
||||
d = 0
|
||||
ce = 0
|
||||
ed = 0
|
||||
for node in graph.nodes
|
||||
d += data(node.task) * length(node.parents)
|
||||
ce += compute_effort(node.task)
|
||||
ed += length(node.parents)
|
||||
end
|
||||
|
||||
ci = ce / d
|
||||
|
||||
result = (
|
||||
data = d,
|
||||
compute_effort = ce,
|
||||
compute_intensity = ci,
|
||||
nodes = length(graph.nodes),
|
||||
edges = ed,
|
||||
)
|
||||
return result
|
||||
end
|
||||
|
||||
"""
|
||||
get_exit_node(graph::DAG)
|
||||
|
||||
Return the graph's exit node. This assumes the graph only has a single exit node. If the graph has multiple exit nodes, the one encountered first will be returned.
|
||||
"""
|
||||
function get_exit_node(graph::DAG)
|
||||
for node in graph.nodes
|
||||
if (is_exit_node(node))
|
||||
return node
|
||||
end
|
||||
end
|
||||
@assert false "The given graph has no exit node! It is either empty or not acyclic!"
|
||||
end
|
73
src/graph/type.jl
Normal file
73
src/graph/type.jl
Normal file
@ -0,0 +1,73 @@
|
||||
using DataStructures
|
||||
|
||||
"""
|
||||
PossibleOperations
|
||||
|
||||
A struct storing all possible operations on a [`DAG`](@ref).
|
||||
To get the [`PossibleOperations`](@ref) on a [`DAG`](@ref), use [`get_operations`](@ref).
|
||||
"""
|
||||
mutable struct PossibleOperations
|
||||
nodeFusions::Set{NodeFusion}
|
||||
nodeReductions::Set{NodeReduction}
|
||||
nodeSplits::Set{NodeSplit}
|
||||
end
|
||||
|
||||
"""
|
||||
DAG
|
||||
|
||||
The representation of the graph as a set of [`Node`](@ref)s.
|
||||
|
||||
A DAG can be loaded using the appropriate parse function, e.g. [`parse_abc`](@ref).
|
||||
|
||||
[`Operation`](@ref)s can be applied on it using [`push_operation!`](@ref) and reverted using [`pop_operation!`](@ref) like a stack.
|
||||
To get the set of possible operations, use [`get_operations`](@ref).
|
||||
The members of the object should not be manually accessed, instead always use the provided interface functions.
|
||||
"""
|
||||
mutable struct DAG
|
||||
nodes::Set{Node}
|
||||
|
||||
# The operations currently applied to the set of nodes
|
||||
appliedOperations::Stack{AppliedOperation}
|
||||
|
||||
# The operations not currently applied but part of the current state of the DAG
|
||||
operationsToApply::Deque{Operation}
|
||||
|
||||
# The possible operations at the current state of the DAG
|
||||
possibleOperations::PossibleOperations
|
||||
|
||||
# The set of nodes whose possible operations need to be reevaluated
|
||||
dirtyNodes::Set{Node}
|
||||
|
||||
# "snapshot" system: keep track of added/removed nodes/edges since last snapshot
|
||||
# these are muted in insert_node! etc.
|
||||
diff::Diff
|
||||
end
|
||||
|
||||
"""
|
||||
PossibleOperations()
|
||||
|
||||
Construct and return an empty [`PossibleOperations`](@ref) object.
|
||||
"""
|
||||
function PossibleOperations()
|
||||
return PossibleOperations(
|
||||
Set{NodeFusion}(),
|
||||
Set{NodeReduction}(),
|
||||
Set{NodeSplit}(),
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
DAG()
|
||||
|
||||
Construct and return an empty [`DAG`](@ref).
|
||||
"""
|
||||
function DAG()
|
||||
return DAG(
|
||||
Set{Node}(),
|
||||
Stack{AppliedOperation}(),
|
||||
Deque{Operation}(),
|
||||
PossibleOperations(),
|
||||
Set{Node}(),
|
||||
Diff(),
|
||||
)
|
||||
end
|
61
src/graph/validate.jl
Normal file
61
src/graph/validate.jl
Normal file
@ -0,0 +1,61 @@
|
||||
"""
|
||||
is_connected(graph::DAG)
|
||||
|
||||
Return whether the given graph is connected.
|
||||
"""
|
||||
function is_connected(graph::DAG)
|
||||
nodeQueue = Deque{Node}()
|
||||
push!(nodeQueue, get_exit_node(graph))
|
||||
seenNodes = Set{Node}()
|
||||
|
||||
while !isempty(nodeQueue)
|
||||
current = pop!(nodeQueue)
|
||||
push!(seenNodes, current)
|
||||
|
||||
for child in current.children
|
||||
push!(nodeQueue, child)
|
||||
end
|
||||
end
|
||||
|
||||
return length(seenNodes) == length(graph.nodes)
|
||||
end
|
||||
|
||||
"""
|
||||
is_valid(graph::DAG)
|
||||
|
||||
Validate the entire graph using asserts. Intended for testing with `@assert is_valid(graph)`.
|
||||
"""
|
||||
function is_valid(graph::DAG)
|
||||
for node in graph.nodes
|
||||
@assert is_valid(graph, node)
|
||||
end
|
||||
|
||||
for op in graph.operationsToApply
|
||||
@assert is_valid(graph, op)
|
||||
end
|
||||
|
||||
for nr in graph.possibleOperations.nodeReductions
|
||||
@assert is_valid(graph, nr)
|
||||
end
|
||||
for ns in graph.possibleOperations.nodeSplits
|
||||
@assert is_valid(graph, ns)
|
||||
end
|
||||
for nf in graph.possibleOperations.nodeFusions
|
||||
@assert is_valid(graph, nf)
|
||||
end
|
||||
|
||||
for node in graph.dirtyNodes
|
||||
@assert node in graph "Dirty Node is not part of the graph!"
|
||||
@assert ismissing(node.nodeReduction) "Dirty Node has a NodeReduction!"
|
||||
@assert ismissing(node.nodeSplit) "Dirty Node has a NodeSplit!"
|
||||
if (typeof(node) <: DataTaskNode)
|
||||
@assert ismissing(node.nodeFusion) "Dirty DataTaskNode has a Node Fusion!"
|
||||
elseif (typeof(node) <: ComputeTaskNode)
|
||||
@assert isempty(node.nodeFusions) "Dirty ComputeTaskNode has Node Fusions!"
|
||||
end
|
||||
end
|
||||
|
||||
@assert is_connected(graph) "Graph is not connected!"
|
||||
|
||||
return true
|
||||
end
|
@ -1,354 +0,0 @@
|
||||
using DataStructures
|
||||
|
||||
in(node::Node, graph::DAG) = node in graph.nodes
|
||||
in(edge::Edge, graph::DAG) = edge in graph.edges
|
||||
|
||||
function is_parent(potential_parent, node)
|
||||
return potential_parent in node.parents
|
||||
end
|
||||
|
||||
function is_child(potential_child, node)
|
||||
return potential_child in node.children
|
||||
end
|
||||
|
||||
function ==(n1::Node, n2::Node, g::DAG)
|
||||
if typeof(n1) != typeof(n2)
|
||||
return false
|
||||
end
|
||||
if !(n1 in g) || !(n2 in g)
|
||||
return false
|
||||
end
|
||||
|
||||
return n1.task == n2.task && children(n1) == children(n2)
|
||||
end
|
||||
|
||||
# children = prerequisite nodes, nodes that need to execute before the task, edges point into this task
|
||||
function children(node::Node)
|
||||
return copy(node.children)
|
||||
end
|
||||
|
||||
# parents = subsequent nodes, nodes that need this node to execute, edges point from this task
|
||||
function parents(node::Node)
|
||||
return copy(node.parents)
|
||||
end
|
||||
|
||||
# siblings = all children of any parents, no duplicates, includes the node itself
|
||||
function siblings(node::Node)
|
||||
result = Set{Node}()
|
||||
push!(result, node)
|
||||
for parent in node.parents
|
||||
union!(result, parent.children)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
# partners = all parents of any children, no duplicates, includes the node itself
|
||||
function partners(node::Node)
|
||||
result = Set{Node}()
|
||||
push!(result, node)
|
||||
for child in node.children
|
||||
union!(result, child.parents)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
# alternative version to partners(Node), avoiding allocation of a new set
|
||||
# works on the given set and returns nothing
|
||||
function partners(node::Node, set::Set{Node})
|
||||
push!(set, node)
|
||||
for child in node.children
|
||||
union!(set, child.parents)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
is_entry_node(node::Node) = length(node.children) == 0
|
||||
is_exit_node(node::Node) = length(node.parents) == 0
|
||||
|
||||
# function to invalidate the operation caches for a given NodeFusion
|
||||
function invalidate_caches!(graph::DAG, operation::NodeFusion)
|
||||
delete!(graph.possibleOperations, operation)
|
||||
|
||||
# delete the operation from all caches of nodes involved in the operation
|
||||
filter!(!=(operation), operation.input[1].nodeFusions)
|
||||
filter!(!=(operation), operation.input[3].nodeFusions)
|
||||
|
||||
operation.input[2].nodeFusion = missing
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# function to invalidate the operation caches for a given NodeReduction
|
||||
function invalidate_caches!(graph::DAG, operation::NodeReduction)
|
||||
delete!(graph.possibleOperations, operation)
|
||||
|
||||
for node in operation.input
|
||||
node.nodeReduction = missing
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# function to invalidate the operation caches for a given NodeSplit
|
||||
function invalidate_caches!(graph::DAG, operation::NodeSplit)
|
||||
delete!(graph.possibleOperations, operation)
|
||||
|
||||
# delete the operation from all caches of nodes involved in the operation
|
||||
# for node split there is only one node
|
||||
operation.input.nodeSplit = missing
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# function to invalidate the operation caches of a ComputeTaskNode
|
||||
function invalidate_operation_caches!(graph::DAG, node::ComputeTaskNode)
|
||||
if !ismissing(node.nodeReduction)
|
||||
invalidate_caches!(graph, node.nodeReduction)
|
||||
end
|
||||
if !ismissing(node.nodeSplit)
|
||||
invalidate_caches!(graph, node.nodeSplit)
|
||||
end
|
||||
while !isempty(node.nodeFusions)
|
||||
invalidate_caches!(graph, pop!(node.nodeFusions))
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
# function to invalidate the operation caches of a DataTaskNode
|
||||
function invalidate_operation_caches!(graph::DAG, node::DataTaskNode)
|
||||
if !ismissing(node.nodeReduction)
|
||||
invalidate_caches!(graph, node.nodeReduction)
|
||||
end
|
||||
if !ismissing(node.nodeSplit)
|
||||
invalidate_caches!(graph, node.nodeSplit)
|
||||
end
|
||||
if !ismissing(node.nodeFusion)
|
||||
invalidate_caches!(graph, node.nodeFusion)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
# for graph mutating functions we need to do a few things
|
||||
# 1: mute the graph (duh)
|
||||
# 2: keep track of what was changed for the diff (if track == true)
|
||||
# 3: invalidate operation caches
|
||||
|
||||
function insert_node!(graph::DAG, node::Node, track=true, invalidate_cache=true)
|
||||
# 1: mute
|
||||
push!(graph.nodes, node)
|
||||
|
||||
# 2: keep track
|
||||
if (track) push!(graph.diff.addedNodes, node) end
|
||||
|
||||
# 3: invalidate caches
|
||||
if (!invalidate_cache) return node end
|
||||
push!(graph.dirtyNodes, node)
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
function insert_edge!(graph::DAG, node1::Node, node2::Node, track=true, invalidate_cache=true)
|
||||
# @assert (node2 ∉ node1.parents) && (node1 ∉ node2.children) "Edge to insert already exists"
|
||||
|
||||
# 1: mute
|
||||
# edge points from child to parent
|
||||
push!(node1.parents, node2)
|
||||
push!(node2.children, node1)
|
||||
|
||||
# 2: keep track
|
||||
if (track) push!(graph.diff.addedEdges, make_edge(node1, node2)) end
|
||||
|
||||
# 3: invalidate caches
|
||||
if (!invalidate_cache) return nothing end
|
||||
|
||||
invalidate_operation_caches!(graph, node1)
|
||||
invalidate_operation_caches!(graph, node2)
|
||||
|
||||
push!(graph.dirtyNodes, node1)
|
||||
push!(graph.dirtyNodes, node2)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
function remove_node!(graph::DAG, node::Node, track=true, invalidate_cache=true)
|
||||
# @assert node in graph.nodes "Trying to remove a node that's not in the graph"
|
||||
|
||||
# 1: mute
|
||||
delete!(graph.nodes, node)
|
||||
|
||||
# 2: keep track
|
||||
if (track) push!(graph.diff.removedNodes, node) end
|
||||
|
||||
# 3: invalidate caches
|
||||
if (!invalidate_cache) return nothing end
|
||||
|
||||
invalidate_operation_caches!(graph, node)
|
||||
delete!(graph.dirtyNodes, node)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
function remove_edge!(graph::DAG, node1::Node, node2::Node, track=true, invalidate_cache=true)
|
||||
# 1: mute
|
||||
pre_length1 = length(node1.parents)
|
||||
pre_length2 = length(node2.children)
|
||||
filter!(x -> x != node2, node1.parents)
|
||||
filter!(x -> x != node1, node2.children)
|
||||
|
||||
#=@assert begin
|
||||
removed = pre_length1 - length(node1.parents)
|
||||
removed <= 1
|
||||
end "removed more than one node from node1's parents"=#
|
||||
|
||||
#=@assert begin
|
||||
removed = pre_length2 - length(node2.children)
|
||||
removed <= 1
|
||||
end "removed more than one node from node2's children"=#
|
||||
|
||||
# 2: keep track
|
||||
if (track) push!(graph.diff.removedEdges, make_edge(node1, node2)) end
|
||||
|
||||
# 3: invalidate caches
|
||||
if (!invalidate_cache) return nothing end
|
||||
|
||||
invalidate_operation_caches!(graph, node1)
|
||||
invalidate_operation_caches!(graph, node2)
|
||||
if (node1 in graph)
|
||||
push!(graph.dirtyNodes, node1)
|
||||
end
|
||||
if (node2 in graph)
|
||||
push!(graph.dirtyNodes, node2)
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# return the graph "difference" since last time this function was called
|
||||
function get_snapshot_diff(graph::DAG)
|
||||
return swapfield!(graph, :diff, Diff())
|
||||
end
|
||||
|
||||
function graph_properties(graph::DAG)
|
||||
# make sure the graph is fully generated
|
||||
apply_all!(graph)
|
||||
|
||||
d = 0
|
||||
ce = 0
|
||||
ed = 0
|
||||
for node in graph.nodes
|
||||
d += data(node.task) * length(node.parents)
|
||||
ce += compute_effort(node.task)
|
||||
ed += length(node.parents)
|
||||
end
|
||||
|
||||
ci = ce / d
|
||||
|
||||
result = (data = d,
|
||||
compute_effort = ce,
|
||||
compute_intensity = ci,
|
||||
nodes = length(graph.nodes),
|
||||
edges = ed)
|
||||
return result
|
||||
end
|
||||
|
||||
function get_exit_node(graph::DAG)
|
||||
for node in graph.nodes
|
||||
if (is_exit_node(node))
|
||||
return node
|
||||
end
|
||||
end
|
||||
@assert false "The given graph has no exit node! It is either empty or not acyclic!"
|
||||
end
|
||||
|
||||
# check whether the given graph is connected
|
||||
function is_valid(graph::DAG)
|
||||
nodeQueue = Deque{Node}()
|
||||
push!(nodeQueue, get_exit_node(graph))
|
||||
seenNodes = Set{Node}()
|
||||
|
||||
while !isempty(nodeQueue)
|
||||
current = pop!(nodeQueue)
|
||||
push!(seenNodes, current)
|
||||
|
||||
for child in current.chlidren
|
||||
push!(nodeQueue, child)
|
||||
end
|
||||
end
|
||||
|
||||
return length(seenNodes) == length(graph.nodes)
|
||||
end
|
||||
|
||||
function show_nodes(io, graph::DAG)
|
||||
print(io, "[")
|
||||
first = true
|
||||
for n in graph.nodes
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
print(io, ", ")
|
||||
end
|
||||
print(io, n)
|
||||
end
|
||||
print(io, "]")
|
||||
end
|
||||
|
||||
function show(io::IO, graph::DAG)
|
||||
println(io, "Graph:")
|
||||
print(io, " Nodes: ")
|
||||
|
||||
nodeDict = Dict{Type, Int64}()
|
||||
noEdges = 0
|
||||
for node in graph.nodes
|
||||
if haskey(nodeDict, typeof(node.task))
|
||||
nodeDict[typeof(node.task)] = nodeDict[typeof(node.task)] + 1
|
||||
else
|
||||
nodeDict[typeof(node.task)] = 1
|
||||
end
|
||||
noEdges += length(parents(node))
|
||||
end
|
||||
|
||||
if length(graph.nodes) <= 20
|
||||
show_nodes(io, graph)
|
||||
else
|
||||
print("Total: ", length(graph.nodes), ", ")
|
||||
first = true
|
||||
i = 0
|
||||
for (type, number) in zip(keys(nodeDict), values(nodeDict))
|
||||
i += 1
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
print(", ")
|
||||
end
|
||||
if (i % 3 == 0)
|
||||
print("\n ")
|
||||
end
|
||||
print(type, ": ", number)
|
||||
end
|
||||
end
|
||||
println(io)
|
||||
println(io, " Edges: ", noEdges)
|
||||
properties = graph_properties(graph)
|
||||
println(io, " Total Compute Effort: ", properties.compute_effort)
|
||||
println(io, " Total Data Transfer: ", properties.data)
|
||||
println(io, " Total Compute Intensity: ", properties.compute_intensity)
|
||||
end
|
||||
|
||||
function show(io::IO, diff::Diff)
|
||||
print(io, "Nodes: ")
|
||||
print(io, length(diff.addedNodes) + length(diff.removedNodes))
|
||||
print(io, " Edges: ")
|
||||
print(io, length(diff.addedEdges) + length(diff.removedEdges))
|
||||
end
|
||||
|
||||
# return a namedtuple of the lengths of the added/removed nodes/edges
|
||||
function length(diff::Diff)
|
||||
return (
|
||||
addedNodes = length(diff.addedNodes),
|
||||
removedNodes = length(diff.removedNodes),
|
||||
addedEdges = length(diff.addedEdges),
|
||||
removedEdges = length(diff.removedEdges)
|
||||
)
|
||||
end
|
@ -1,34 +0,0 @@
|
||||
# user interface on the DAG
|
||||
|
||||
# applies a new operation to the end of the graph
|
||||
function push_operation!(graph::DAG, operation::Operation)
|
||||
# 1.: Add the operation to the DAG
|
||||
push!(graph.operationsToApply, operation)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# reverts the latest applied operation, essentially like a ctrl+z for
|
||||
function pop_operation!(graph::DAG)
|
||||
# 1.: Remove the operation from the appliedChain of the DAG
|
||||
if !isempty(graph.operationsToApply)
|
||||
pop!(graph.operationsToApply)
|
||||
elseif !isempty(graph.appliedOperations)
|
||||
appliedOp = pop!(graph.appliedOperations)
|
||||
revert_operation!(graph, appliedOp)
|
||||
else
|
||||
error("No more operations to pop!")
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
can_pop(graph::DAG) = !isempty(graph.operationsToApply) || !isempty(graph.appliedOperations)
|
||||
|
||||
# reset the graph to its initial state with no operations applied
|
||||
function reset_graph!(graph::DAG)
|
||||
while (can_pop(graph))
|
||||
pop_operation!(graph)
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
@ -1,11 +1,14 @@
|
||||
using Printf
|
||||
|
||||
# functions for importing DAGs from a file
|
||||
regex_a = r"^[A-C]\d+$" # Regex for the initial particles
|
||||
regex_c = r"^[A-C]\(([^']*),([^']*)\)$" # Regex for the combinations of 2 particles
|
||||
regex_m = r"^M\(([^']*),([^']*),([^']*)\)$" # Regex for the combinations of 3 particles
|
||||
regex_plus = r"^\+$" # Regex for the sum
|
||||
|
||||
"""
|
||||
parse_nodes(input::AbstractString)
|
||||
|
||||
Parse the given string into a vector of strings containing each node.
|
||||
"""
|
||||
function parse_nodes(input::AbstractString)
|
||||
regex = r"'([^']*)'"
|
||||
matches = eachmatch(regex, input)
|
||||
@ -13,6 +16,11 @@ function parse_nodes(input::AbstractString)
|
||||
return output
|
||||
end
|
||||
|
||||
"""
|
||||
parse_edges(input::AbstractString)
|
||||
|
||||
Parse the given string into a vector of strings containing each edge. Currently unused since the entire graph can be read from just the node names.
|
||||
"""
|
||||
function parse_edges(input::AbstractString)
|
||||
regex = r"\('([^']*)', '([^']*)'\)"
|
||||
matches = eachmatch(regex, input)
|
||||
@ -20,24 +28,36 @@ function parse_edges(input::AbstractString)
|
||||
return output
|
||||
end
|
||||
|
||||
# reads an abc-model process from the given file
|
||||
"""
|
||||
parse_abc(filename::String; verbose::Bool = false)
|
||||
|
||||
Read an abc-model process from the given file. If `verbose` is set to true, print some progress information to stdout.
|
||||
|
||||
Returns a valid [`DAG`](@ref).
|
||||
"""
|
||||
function parse_abc(filename::String, verbose::Bool = false)
|
||||
file = open(filename, "r")
|
||||
|
||||
if (verbose) println("Opened file") end
|
||||
if (verbose)
|
||||
println("Opened file")
|
||||
end
|
||||
nodes_string = readline(file)
|
||||
nodes = parse_nodes(nodes_string)
|
||||
|
||||
close(file)
|
||||
if (verbose) println("Read file") end
|
||||
if (verbose)
|
||||
println("Read file")
|
||||
end
|
||||
|
||||
graph = DAG()
|
||||
|
||||
|
||||
# estimate total number of nodes
|
||||
# try to slightly overestimate so no resizing is necessary
|
||||
# data nodes are not included in length(nodes) and there are a few more than compute nodes
|
||||
estimate_no_nodes = round(Int, length(nodes) * 4)
|
||||
if (verbose) println("Estimating ", estimate_no_nodes, " Nodes") end
|
||||
if (verbose)
|
||||
println("Estimating ", estimate_no_nodes, " Nodes")
|
||||
end
|
||||
sizehint!(graph.nodes, estimate_no_nodes)
|
||||
|
||||
sum_node = insert_node!(graph, make_node(ComputeTaskSum()), false, false)
|
||||
@ -47,43 +67,60 @@ function parse_abc(filename::String, verbose::Bool = false)
|
||||
# remember the data out nodes for connection
|
||||
dataOutNodes = Dict()
|
||||
|
||||
if (verbose) println("Building graph") end
|
||||
if (verbose)
|
||||
println("Building graph")
|
||||
end
|
||||
noNodes = 0
|
||||
nodesToRead = length(nodes)
|
||||
while !isempty(nodes)
|
||||
node = popfirst!(nodes)
|
||||
noNodes += 1
|
||||
if (noNodes % 100 == 0)
|
||||
if (verbose) @printf "\rReading Nodes... %.2f%%" (100. * noNodes / nodesToRead) end
|
||||
if (verbose)
|
||||
percent = string(
|
||||
round(100.0 * noNodes / nodesToRead, digits = 2),
|
||||
"%",
|
||||
)
|
||||
print("\rReading Nodes... $percent")
|
||||
end
|
||||
end
|
||||
if occursin(regex_a, node)
|
||||
# add nodes and edges for the state reading to u(P(Particle))
|
||||
data_in = insert_node!(graph, make_node(DataTask(4)), false, false) # read particle data node
|
||||
compute_P = insert_node!(graph, make_node(ComputeTaskP()), false, false) # compute P node
|
||||
compute_P =
|
||||
insert_node!(graph, make_node(ComputeTaskP()), false, false) # compute P node
|
||||
data_Pu = insert_node!(graph, make_node(DataTask(6)), false, false) # transfer data from P to u
|
||||
compute_u = insert_node!(graph, make_node(ComputeTaskU()), false, false) # compute U node
|
||||
compute_u =
|
||||
insert_node!(graph, make_node(ComputeTaskU()), false, false) # compute U node
|
||||
data_out = insert_node!(graph, make_node(DataTask(3)), false, false) # transfer data out from u
|
||||
|
||||
insert_edge!(graph, data_in, compute_P, false, false)
|
||||
insert_edge!(graph, compute_P, data_Pu, false, false)
|
||||
insert_edge!(graph, data_Pu, compute_u, false, false)
|
||||
insert_edge!(graph, compute_u, data_out, false, false)
|
||||
|
||||
|
||||
# remember the data_out node for future edges
|
||||
dataOutNodes[node] = data_out
|
||||
elseif occursin(regex_c, node)
|
||||
capt = match(regex_c, node)
|
||||
|
||||
|
||||
in1 = capt.captures[1]
|
||||
in2 = capt.captures[2]
|
||||
|
||||
compute_v = insert_node!(graph, make_node(ComputeTaskV()), false, false)
|
||||
compute_v =
|
||||
insert_node!(graph, make_node(ComputeTaskV()), false, false)
|
||||
data_out = insert_node!(graph, make_node(DataTask(5)), false, false)
|
||||
|
||||
if (occursin(regex_c, in1))
|
||||
# put an S node after this input
|
||||
compute_S = insert_node!(graph, make_node(ComputeTaskS1()), false, false)
|
||||
data_S_v = insert_node!(graph, make_node(DataTask(5)), false, false)
|
||||
compute_S = insert_node!(
|
||||
graph,
|
||||
make_node(ComputeTaskS1()),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
data_S_v =
|
||||
insert_node!(graph, make_node(DataTask(5)), false, false)
|
||||
|
||||
insert_edge!(graph, dataOutNodes[in1], compute_S, false, false)
|
||||
insert_edge!(graph, compute_S, data_S_v, false, false)
|
||||
@ -96,8 +133,14 @@ function parse_abc(filename::String, verbose::Bool = false)
|
||||
if (occursin(regex_c, in2))
|
||||
# i think the current generator only puts the combined particles in the first space, so this case might never be entered
|
||||
# put an S node after this input
|
||||
compute_S = insert_node!(graph, make_node(ComputeTaskS1()), false, false)
|
||||
data_S_v = insert_node!(graph, make_node(DataTask(5)), false, false)
|
||||
compute_S = insert_node!(
|
||||
graph,
|
||||
make_node(ComputeTaskS1()),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
data_S_v =
|
||||
insert_node!(graph, make_node(DataTask(5)), false, false)
|
||||
|
||||
insert_edge!(graph, dataOutNodes[in2], compute_S, false, false)
|
||||
insert_edge!(graph, compute_S, data_S_v, false, false)
|
||||
@ -106,7 +149,7 @@ function parse_abc(filename::String, verbose::Bool = false)
|
||||
else
|
||||
insert_edge!(graph, dataOutNodes[in2], compute_v, false, false)
|
||||
end
|
||||
|
||||
|
||||
insert_edge!(graph, compute_v, data_out, false, false)
|
||||
dataOutNodes[node] = data_out
|
||||
|
||||
@ -118,7 +161,8 @@ function parse_abc(filename::String, verbose::Bool = false)
|
||||
in3 = capt.captures[3]
|
||||
|
||||
# in2 + in3 with a v
|
||||
compute_v = insert_node!(graph, make_node(ComputeTaskV()), false, false)
|
||||
compute_v =
|
||||
insert_node!(graph, make_node(ComputeTaskV()), false, false)
|
||||
data_v = insert_node!(graph, make_node(DataTask(5)), false, false)
|
||||
|
||||
insert_edge!(graph, dataOutNodes[in2], compute_v, false, false)
|
||||
@ -126,8 +170,10 @@ function parse_abc(filename::String, verbose::Bool = false)
|
||||
insert_edge!(graph, compute_v, data_v, false, false)
|
||||
|
||||
# combine with the v of the combined other input
|
||||
compute_S2 = insert_node!(graph, make_node(ComputeTaskS2()), false, false)
|
||||
data_out = insert_node!(graph, make_node(DataTask(10)), false, false)
|
||||
compute_S2 =
|
||||
insert_node!(graph, make_node(ComputeTaskS2()), false, false)
|
||||
data_out =
|
||||
insert_node!(graph, make_node(DataTask(10)), false, false)
|
||||
|
||||
insert_edge!(graph, data_v, compute_S2, false, false)
|
||||
insert_edge!(graph, dataOutNodes[in1], compute_S2, false, false)
|
||||
@ -136,11 +182,13 @@ function parse_abc(filename::String, verbose::Bool = false)
|
||||
insert_edge!(graph, data_out, sum_node, false, false)
|
||||
elseif occursin(regex_plus, node)
|
||||
if (verbose)
|
||||
println("\rReading Nodes Complete ")
|
||||
println("\rReading Nodes Complete ")
|
||||
println("Added ", length(graph.nodes), " nodes")
|
||||
end
|
||||
else
|
||||
@assert false ("Unknown node '$node' while reading from file $filename")
|
||||
@assert false (
|
||||
"Unknown node '$node' while reading from file $filename"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
102
src/models/abc/properties.jl
Normal file
102
src/models/abc/properties.jl
Normal file
@ -0,0 +1,102 @@
|
||||
"""
|
||||
compute_effort(t::ComputeTaskS1)
|
||||
|
||||
Return the compute effort of an S1 task.
|
||||
"""
|
||||
compute_effort(t::ComputeTaskS1) = 10
|
||||
|
||||
"""
|
||||
compute_effort(t::ComputeTaskS2)
|
||||
|
||||
Return the compute effort of an S2 task.
|
||||
"""
|
||||
compute_effort(t::ComputeTaskS2) = 10
|
||||
|
||||
"""
|
||||
compute_effort(t::ComputeTaskU)
|
||||
|
||||
Return the compute effort of a U task.
|
||||
"""
|
||||
compute_effort(t::ComputeTaskU) = 6
|
||||
|
||||
"""
|
||||
compute_effort(t::ComputeTaskV)
|
||||
|
||||
Return the compute effort of a V task.
|
||||
"""
|
||||
compute_effort(t::ComputeTaskV) = 20
|
||||
|
||||
"""
|
||||
compute_effort(t::ComputeTaskP)
|
||||
|
||||
Return the compute effort of a P task.
|
||||
"""
|
||||
compute_effort(t::ComputeTaskP) = 15
|
||||
|
||||
"""
|
||||
compute_effort(t::ComputeTaskSum)
|
||||
|
||||
Return the compute effort of a Sum task.
|
||||
|
||||
Note: This is a constant compute effort, even though sum scales with the number of its inputs. Since there is only ever a single sum node in a graph generated from the ABC-Model,
|
||||
this doesn't matter.
|
||||
"""
|
||||
compute_effort(t::ComputeTaskSum) = 1
|
||||
|
||||
"""
|
||||
show(io::IO, t::DataTask)
|
||||
|
||||
Print the data task to io.
|
||||
"""
|
||||
function show(io::IO, t::DataTask)
|
||||
return print(io, "Data", t.data)
|
||||
end
|
||||
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskS1)
|
||||
|
||||
Print the S1 task to io.
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskS1) = print("ComputeS1")
|
||||
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskS2)
|
||||
|
||||
Print the S2 task to io.
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskS2) = print("ComputeS2")
|
||||
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskP)
|
||||
|
||||
Print the P task to io.
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskP) = print("ComputeP")
|
||||
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskU)
|
||||
|
||||
Print the U task to io.
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskU) = print("ComputeU")
|
||||
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskV)
|
||||
|
||||
Print the V task to io.
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskV) = print("ComputeV")
|
||||
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskSum)
|
||||
|
||||
Print the sum task to io.
|
||||
"""
|
||||
show(io::IO, t::ComputeTaskSum) = print("ComputeSum")
|
||||
|
||||
"""
|
||||
copy(t::DataTask)
|
||||
|
||||
Copy the data task and return it.
|
||||
"""
|
||||
copy(t::DataTask) = DataTask(t.data)
|
65
src/models/abc/types.jl
Normal file
65
src/models/abc/types.jl
Normal file
@ -0,0 +1,65 @@
|
||||
"""
|
||||
DataTask <: AbstractDataTask
|
||||
|
||||
Task representing a specific data transfer in the ABC Model.
|
||||
"""
|
||||
struct DataTask <: AbstractDataTask
|
||||
data::UInt64
|
||||
end
|
||||
|
||||
"""
|
||||
ComputeTaskS1 <: AbstractComputeTask
|
||||
|
||||
S task with a single child.
|
||||
"""
|
||||
struct ComputeTaskS1 <: AbstractComputeTask end
|
||||
|
||||
"""
|
||||
ComputeTaskS2 <: AbstractComputeTask
|
||||
|
||||
S task with two children.
|
||||
"""
|
||||
struct ComputeTaskS2 <: AbstractComputeTask end
|
||||
|
||||
"""
|
||||
ComputeTaskP <: AbstractComputeTask
|
||||
|
||||
P task with no children.
|
||||
"""
|
||||
struct ComputeTaskP <: AbstractComputeTask end
|
||||
|
||||
"""
|
||||
ComputeTaskV <: AbstractComputeTask
|
||||
|
||||
v task with two children.
|
||||
"""
|
||||
struct ComputeTaskV <: AbstractComputeTask end
|
||||
|
||||
"""
|
||||
ComputeTaskU <: AbstractComputeTask
|
||||
|
||||
u task with a single child.
|
||||
"""
|
||||
struct ComputeTaskU <: AbstractComputeTask end
|
||||
|
||||
"""
|
||||
ComputeTaskSum <: AbstractComputeTask
|
||||
|
||||
Task that sums all its inputs, n children.
|
||||
"""
|
||||
struct ComputeTaskSum <: AbstractComputeTask end
|
||||
|
||||
"""
|
||||
ABC_TASKS
|
||||
|
||||
Constant vector of all tasks of the ABC-Model.
|
||||
"""
|
||||
ABC_TASKS = [
|
||||
DataTask,
|
||||
ComputeTaskS1,
|
||||
ComputeTaskS2,
|
||||
ComputeTaskP,
|
||||
ComputeTaskV,
|
||||
ComputeTaskU,
|
||||
ComputeTaskSum,
|
||||
]
|
35
src/node/compare.jl
Normal file
35
src/node/compare.jl
Normal file
@ -0,0 +1,35 @@
|
||||
"""
|
||||
==(e1::Edge, e2::Edge)
|
||||
|
||||
Equality comparison between two edges.
|
||||
"""
|
||||
function ==(e1::Edge, e2::Edge)
|
||||
return e1.edge[1] == e2.edge[1] && e1.edge[2] == e2.edge[2]
|
||||
end
|
||||
|
||||
"""
|
||||
==(n1::Node, n2::Node)
|
||||
|
||||
Fallback equality comparison between two nodes. For equal node types, the more specific versions of this function will be called.
|
||||
"""
|
||||
function ==(n1::Node, n2::Node)
|
||||
return false
|
||||
end
|
||||
|
||||
"""
|
||||
==(n1::ComputeTaskNode, n2::ComputeTaskNode)
|
||||
|
||||
Equality comparison between two [`ComputeTaskNode`](@ref)s.
|
||||
"""
|
||||
function ==(n1::ComputeTaskNode, n2::ComputeTaskNode)
|
||||
return n1.id == n2.id
|
||||
end
|
||||
|
||||
"""
|
||||
==(n1::DataTaskNode, n2::DataTaskNode)
|
||||
|
||||
Equality comparison between two [`DataTaskNode`](@ref)s.
|
||||
"""
|
||||
function ==(n1::DataTaskNode, n2::DataTaskNode)
|
||||
return n1.id == n2.id
|
||||
end
|
53
src/node/create.jl
Normal file
53
src/node/create.jl
Normal file
@ -0,0 +1,53 @@
|
||||
"""
|
||||
make_node(t::AbstractTask)
|
||||
|
||||
Fallback implementation of `make_node` for an [`AbstractTask`](@ref), throwing an error.
|
||||
"""
|
||||
function make_node(t::AbstractTask)
|
||||
return error("Cannot make a node from this task type")
|
||||
end
|
||||
|
||||
"""
|
||||
make_node(t::AbstractDataTask)
|
||||
|
||||
Construct and return a new [`DataTaskNode`](@ref) with the given task.
|
||||
"""
|
||||
function make_node(t::AbstractDataTask)
|
||||
return DataTaskNode(t)
|
||||
end
|
||||
|
||||
"""
|
||||
make_node(t::AbstractComputeTask)
|
||||
|
||||
Construct and return a new [`ComputeTaskNode`](@ref) with the given task.
|
||||
"""
|
||||
function make_node(t::AbstractComputeTask)
|
||||
return ComputeTaskNode(t)
|
||||
end
|
||||
|
||||
"""
|
||||
make_edge(n1::Node, n2::Node)
|
||||
|
||||
Fallback implementation of `make_edge` throwing an error. If you got this error it likely means you tried to construct an edge between two nodes of the same type.
|
||||
"""
|
||||
function make_edge(n1::Node, n2::Node)
|
||||
return error("Can only create edges from compute to data node or reverse")
|
||||
end
|
||||
|
||||
"""
|
||||
make_edge(n1::ComputeTaskNode, n2::DataTaskNode)
|
||||
|
||||
Construct and return a new [`Edge`](@ref) pointing from `n1` (child) to `n2` (parent).
|
||||
"""
|
||||
function make_edge(n1::ComputeTaskNode, n2::DataTaskNode)
|
||||
return Edge((n1, n2))
|
||||
end
|
||||
|
||||
"""
|
||||
make_edge(n1::DataTaskNode, n2::ComputeTaskNode)
|
||||
|
||||
Construct and return a new [`Edge`](@ref) pointing from `n1` (child) to `n2` (parent).
|
||||
"""
|
||||
function make_edge(n1::DataTaskNode, n2::ComputeTaskNode)
|
||||
return Edge((n1, n2))
|
||||
end
|
17
src/node/print.jl
Normal file
17
src/node/print.jl
Normal file
@ -0,0 +1,17 @@
|
||||
"""
|
||||
show(io::IO, n::Node)
|
||||
|
||||
Print a short string representation of the node to io.
|
||||
"""
|
||||
function show(io::IO, n::Node)
|
||||
return print(io, "Node(", n.task, ")")
|
||||
end
|
||||
|
||||
"""
|
||||
show(io::IO, e::Edge)
|
||||
|
||||
Print a short string representation of the edge to io.
|
||||
"""
|
||||
function show(io::IO, e::Edge)
|
||||
return print(io, "Edge(", e.edge[1], ", ", e.edge[2], ")")
|
||||
end
|
103
src/node/properties.jl
Normal file
103
src/node/properties.jl
Normal file
@ -0,0 +1,103 @@
|
||||
"""
|
||||
is_entry_node(node::Node)
|
||||
|
||||
Return whether this node is an entry node in its graph, i.e., it has no children.
|
||||
"""
|
||||
is_entry_node(node::Node) = length(node.children) == 0
|
||||
|
||||
"""
|
||||
is_exit_node(node::Node)
|
||||
|
||||
Return whether this node is an exit node of its graph, i.e., it has no parents.
|
||||
"""
|
||||
is_exit_node(node::Node) = length(node.parents) == 0
|
||||
|
||||
"""
|
||||
children(node::Node)
|
||||
|
||||
Return a copy of the node's children so it can safely be muted without changing the node in the graph.
|
||||
|
||||
A node's children are its prerequisite nodes, nodes that need to execute before the task of this node.
|
||||
"""
|
||||
function children(node::Node)
|
||||
return copy(node.children)
|
||||
end
|
||||
|
||||
"""
|
||||
parents(node::Node)
|
||||
|
||||
Return a copy of the node's parents so it can safely be muted without changing the node in the graph.
|
||||
|
||||
A node's parents are its subsequent nodes, nodes that need this node to execute.
|
||||
"""
|
||||
function parents(node::Node)
|
||||
return copy(node.parents)
|
||||
end
|
||||
|
||||
"""
|
||||
siblings(node::Node)
|
||||
|
||||
Return a vector of all siblings of this node.
|
||||
|
||||
A node's siblings are all children of any of its parents. The result contains no duplicates and includes the node itself.
|
||||
"""
|
||||
function siblings(node::Node)
|
||||
result = Set{Node}()
|
||||
push!(result, node)
|
||||
for parent in node.parents
|
||||
union!(result, parent.children)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
"""
|
||||
partners(node::Node)
|
||||
|
||||
Return a vector of all partners of this node.
|
||||
|
||||
A node's partners are all parents of any of its children. The result contains no duplicates and includes the node itself.
|
||||
|
||||
Note: This is very slow when there are multiple children with many parents.
|
||||
This is less of a problem in [`siblings(node::Node)`](@ref) because (depending on the model) there are no nodes with a large number of children, or only a single one.
|
||||
"""
|
||||
function partners(node::Node)
|
||||
result = Set{Node}()
|
||||
push!(result, node)
|
||||
for child in node.children
|
||||
union!(result, child.parents)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
"""
|
||||
partners(node::Node, set::Set{Node})
|
||||
|
||||
Alternative version to [`partners(node::Node)`](@ref), avoiding allocation of a new set. Works on the given set and returns `nothing`.
|
||||
"""
|
||||
function partners(node::Node, set::Set{Node})
|
||||
push!(set, node)
|
||||
for child in node.children
|
||||
union!(set, child.parents)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
is_parent(potential_parent::Node, node::Node)
|
||||
|
||||
Return whether the `potential_parent` is a parent of `node`.
|
||||
"""
|
||||
function is_parent(potential_parent::Node, node::Node)
|
||||
return potential_parent in node.parents
|
||||
end
|
||||
|
||||
"""
|
||||
is_child(potential_child::Node, node::Node)
|
||||
|
||||
Return whether the `potential_child` is a child of `node`.
|
||||
"""
|
||||
function is_child(potential_child::Node, node::Node)
|
||||
return potential_child in node.children
|
||||
end
|
138
src/node/type.jl
Normal file
138
src/node/type.jl
Normal file
@ -0,0 +1,138 @@
|
||||
using Random
|
||||
using UUIDs
|
||||
using Base.Threads
|
||||
|
||||
# TODO: reliably find out how many threads we're running with (nthreads() returns 1 when precompiling :/)
|
||||
rng = [Random.MersenneTwister(0) for _ in 1:32]
|
||||
|
||||
"""
|
||||
Node
|
||||
|
||||
The abstract base type of every node.
|
||||
|
||||
See [`DataTaskNode`](@ref), [`ComputeTaskNode`](@ref) and [`make_node`](@ref).
|
||||
"""
|
||||
abstract type Node end
|
||||
|
||||
# declare this type here because it's needed
|
||||
# the specific operations are declared in graph.jl
|
||||
abstract type Operation end
|
||||
|
||||
"""
|
||||
DataTaskNode <: Node
|
||||
|
||||
Any node that transfers data and does no computation.
|
||||
|
||||
# Fields
|
||||
`.task`: The node's data task type. Usually [`DataTask`](@ref).\\
|
||||
`.parents`: A vector of the node's parents (i.e. nodes that depend on this one).\\
|
||||
`.children`: A vector of the node's children (i.e. nodes that this one depends on).\\
|
||||
`.id`: The node's id. Improves the speed of comparisons.\\
|
||||
`.nodeReduction`: Either this node's [`NodeReduction`](@ref) or `missing`, if none. There can only be at most one.\\
|
||||
`.nodeSplit`: Either this node's [`NodeSplit`](@ref) or `missing`, if none. There can only be at most one.\\
|
||||
`.nodeFusion`: Either this node's [`NodeFusion`](@ref) or `missing`, if none. There can only be at most one for DataTaskNodes.
|
||||
"""
|
||||
mutable struct DataTaskNode <: Node
|
||||
task::AbstractDataTask
|
||||
|
||||
# use vectors as sets have way too much memory overhead
|
||||
parents::Vector{Node}
|
||||
children::Vector{Node}
|
||||
|
||||
# need a unique identifier unique to every *constructed* node
|
||||
# however, it can be copied when splitting a node
|
||||
id::Base.UUID
|
||||
|
||||
# the NodeReduction involving this node, if it exists
|
||||
# Can't use the NodeReduction type here because it's not yet defined
|
||||
nodeReduction::Union{Operation, Missing}
|
||||
|
||||
# the NodeSplit involving this node, if it exists
|
||||
nodeSplit::Union{Operation, Missing}
|
||||
|
||||
# the node fusion involving this node, if it exists
|
||||
nodeFusion::Union{Operation, Missing}
|
||||
end
|
||||
|
||||
"""
|
||||
ComputeTaskNode <: Node
|
||||
|
||||
Any node that transfers data and does no computation.
|
||||
|
||||
# Fields
|
||||
`.task`: The node's data task type. Usually [`DataTask`](@ref).\\
|
||||
`.parents`: A vector of the node's parents (i.e. nodes that depend on this one).\\
|
||||
`.children`: A vector of the node's children (i.e. nodes that this one depends on).\\
|
||||
`.id`: The node's id. Improves the speed of comparisons.\\
|
||||
`.nodeReduction`: Either this node's [`NodeReduction`](@ref) or `missing`, if none. There can only be at most one.\\
|
||||
`.nodeSplit`: Either this node's [`NodeSplit`](@ref) or `missing`, if none. There can only be at most one.\\
|
||||
`.nodeFusion`: A vector of this node's [`NodeFusion`](@ref)s. For a ComputeTaskNode there can be any number of these, unlike the DataTaskNodes.
|
||||
"""
|
||||
mutable struct ComputeTaskNode <: Node
|
||||
task::AbstractComputeTask
|
||||
parents::Vector{Node}
|
||||
children::Vector{Node}
|
||||
id::Base.UUID
|
||||
|
||||
nodeReduction::Union{Operation, Missing}
|
||||
nodeSplit::Union{Operation, Missing}
|
||||
|
||||
# for ComputeTasks there can be multiple fusions, unlike the DataTasks
|
||||
nodeFusions::Vector{Operation}
|
||||
end
|
||||
|
||||
DataTaskNode(t::AbstractDataTask) = DataTaskNode(
|
||||
t,
|
||||
Vector{Node}(),
|
||||
Vector{Node}(),
|
||||
UUIDs.uuid1(rng[threadid()]),
|
||||
missing,
|
||||
missing,
|
||||
missing,
|
||||
)
|
||||
ComputeTaskNode(t::AbstractComputeTask) = ComputeTaskNode(
|
||||
t,
|
||||
Vector{Node}(),
|
||||
Vector{Node}(),
|
||||
UUIDs.uuid1(rng[threadid()]),
|
||||
missing,
|
||||
missing,
|
||||
Vector{NodeFusion}(),
|
||||
)
|
||||
|
||||
"""
|
||||
Edge
|
||||
|
||||
Type of an edge in the graph. Edges can only exist between a [`DataTaskNode`](@ref) and a [`ComputeTaskNode`](@ref) or vice versa, not between two of the same type of node.
|
||||
|
||||
An edge always points from child to parent: `child = e.edge[1]` and `parent = e.edge[2]`.
|
||||
|
||||
The child is the prerequisite node of the parent.
|
||||
"""
|
||||
struct Edge
|
||||
# edge points from child to parent
|
||||
edge::Union{
|
||||
Tuple{DataTaskNode, ComputeTaskNode},
|
||||
Tuple{ComputeTaskNode, DataTaskNode},
|
||||
}
|
||||
end
|
||||
|
||||
copy(m::Missing) = missing
|
||||
copy(n::ComputeTaskNode) = ComputeTaskNode(
|
||||
copy(n.task),
|
||||
copy(n.parents),
|
||||
copy(n.children),
|
||||
UUIDs.uuid1(rng[threadid()]),
|
||||
copy(n.nodeReduction),
|
||||
copy(n.nodeSplit),
|
||||
copy(n.nodeFusions),
|
||||
)
|
||||
copy(n::DataTaskNode) = DataTaskNode(
|
||||
copy(n.task),
|
||||
copy(n.parents),
|
||||
copy(n.children),
|
||||
UUIDs.uuid1(rng[threadid()]),
|
||||
copy(n.nodeReduction),
|
||||
copy(n.nodeSplit),
|
||||
copy(n.nodeFusion),
|
||||
)
|
64
src/node/validate.jl
Normal file
64
src/node/validate.jl
Normal file
@ -0,0 +1,64 @@
|
||||
"""
|
||||
is_valid_node(graph::DAG, node::Node)
|
||||
|
||||
Verify that a given node is valid in the graph. Call like `@test is_valid_node(g, n)`. Uses `@assert` to fail if something is invalid but also provide an error message.
|
||||
|
||||
This function is very performance intensive and should only be used when testing or debugging.
|
||||
|
||||
See also this function's specific versions for the concrete Node types [`is_valid(graph::DAG, node::ComputeTaskNode)`](@ref) and [`is_valid(graph::DAG, node::DataTaskNode)`](@ref).
|
||||
"""
|
||||
function is_valid_node(graph::DAG, node::Node)
|
||||
@assert node in graph "Node is not part of the given graph!"
|
||||
|
||||
for parent in node.parents
|
||||
@assert typeof(parent) != typeof(node) "Node's type is the same as its parent's!"
|
||||
@assert parent in graph "Node's parent is not in the same graph!"
|
||||
@assert node in parent.children "Node is not a child of its parent!"
|
||||
end
|
||||
|
||||
for child in node.children
|
||||
@assert typeof(child) != typeof(node) "Node's type is the same as its child's!"
|
||||
@assert child in graph "Node's child is not in the same graph!"
|
||||
@assert node in child.parents "Node is not a parent of its child!"
|
||||
end
|
||||
|
||||
if !ismissing(node.nodeReduction)
|
||||
@assert is_valid(graph, node.nodeReduction)
|
||||
end
|
||||
if !ismissing(node.nodeSplit)
|
||||
@assert is_valid(graph, node.nodeSplit)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
"""
|
||||
is_valid(graph::DAG, node::ComputeTaskNode)
|
||||
|
||||
Verify that the given compute node is valid in the graph. Call with `@assert` or `@test` when testing or debugging.
|
||||
|
||||
This also calls [`is_valid_node(graph::DAG, node::Node)`](@ref).
|
||||
"""
|
||||
function is_valid(graph::DAG, node::ComputeTaskNode)
|
||||
@assert is_valid_node(graph, node)
|
||||
|
||||
for nf in node.nodeFusions
|
||||
@assert is_valid(graph, nf)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
"""
|
||||
is_valid(graph::DAG, node::DataTaskNode)
|
||||
|
||||
Verify that the given compute node is valid in the graph. Call with `@assert` or `@test` when testing or debugging.
|
||||
|
||||
This also calls [`is_valid_node(graph::DAG, node::Node)`](@ref).
|
||||
"""
|
||||
function is_valid(graph::DAG, node::DataTaskNode)
|
||||
@assert is_valid_node(graph, node)
|
||||
|
||||
if !ismissing(node.nodeFusion)
|
||||
@assert is_valid(graph, node.nodeFusion)
|
||||
end
|
||||
return true
|
||||
end
|
@ -1,51 +0,0 @@
|
||||
function make_node(t::AbstractTask)
|
||||
error("Cannot make a node from this task type")
|
||||
end
|
||||
|
||||
function make_node(t::AbstractDataTask)
|
||||
return DataTaskNode(t)
|
||||
end
|
||||
|
||||
function make_node(t::AbstractComputeTask)
|
||||
return ComputeTaskNode(t)
|
||||
end
|
||||
|
||||
function make_edge(n1::Node, n2::Node)
|
||||
error("Can only create edges from compute to data node or reverse")
|
||||
end
|
||||
|
||||
function make_edge(n1::ComputeTaskNode, n2::DataTaskNode)
|
||||
return Edge((n1, n2))
|
||||
end
|
||||
|
||||
function make_edge(n1::DataTaskNode, n2::ComputeTaskNode)
|
||||
return Edge((n1, n2))
|
||||
end
|
||||
|
||||
function show(io::IO, n::Node)
|
||||
print(io, "Node(", n.task, ")")
|
||||
end
|
||||
|
||||
function show(io::IO, e::Edge)
|
||||
print(io, "Edge(", e.edge[1], ", ", e.edge[2], ")")
|
||||
end
|
||||
|
||||
function ==(e1::Edge, e2::Edge)
|
||||
return e1.edge[1] == e2.edge[1] && e1.edge[2] == e2.edge[2]
|
||||
end
|
||||
|
||||
function ==(n1::Node, n2::Node)
|
||||
return false
|
||||
end
|
||||
|
||||
function ==(n1::ComputeTaskNode, n2::ComputeTaskNode)
|
||||
return n1.id == n2.id
|
||||
end
|
||||
|
||||
function ==(n1::DataTaskNode, n2::DataTaskNode)
|
||||
return n1.id == n2.id
|
||||
end
|
||||
|
||||
copy(m::Missing) = missing
|
||||
copy(n::ComputeTaskNode) = ComputeTaskNode(copy(n.task), copy(n.parents), copy(n.children), UUIDs.uuid1(rng[threadid()]), copy(n.nodeReduction), copy(n.nodeSplit), copy(n.nodeFusions))
|
||||
copy(n::DataTaskNode) = DataTaskNode(copy(n.task), copy(n.parents), copy(n.children), UUIDs.uuid1(rng[threadid()]), copy(n.nodeReduction), copy(n.nodeSplit), copy(n.nodeFusion))
|
56
src/nodes.jl
56
src/nodes.jl
@ -1,56 +0,0 @@
|
||||
using Random
|
||||
using UUIDs
|
||||
using Base.Threads
|
||||
|
||||
# TODO: reliably find out how many threads we're running with (nthreads() returns 1 when precompiling :/)
|
||||
rng = [Random.MersenneTwister(0) for _ in 1:32]
|
||||
|
||||
abstract type Node end
|
||||
|
||||
# declare this type here because it's needed
|
||||
# the specific operations are declared in graph.jl
|
||||
abstract type Operation end
|
||||
|
||||
mutable struct DataTaskNode <: Node
|
||||
task::AbstractDataTask
|
||||
|
||||
# use vectors as sets have way too much memory overhead
|
||||
parents::Vector{Node}
|
||||
children::Vector{Node}
|
||||
|
||||
# need a unique identifier unique to every *constructed* node
|
||||
# however, it can be copied when splitting a node
|
||||
id::Base.UUID
|
||||
|
||||
# the NodeReduction involving this node, if it exists
|
||||
# Can't use the NodeReduction type here because it's not yet defined
|
||||
nodeReduction::Union{Operation, Missing}
|
||||
|
||||
# the NodeSplit involving this node, if it exists
|
||||
nodeSplit::Union{Operation, Missing}
|
||||
|
||||
# the node fusion involving this node, if it exists
|
||||
nodeFusion::Union{Operation, Missing}
|
||||
end
|
||||
|
||||
# same as DataTaskNode
|
||||
mutable struct ComputeTaskNode <: Node
|
||||
task::AbstractComputeTask
|
||||
parents::Vector{Node}
|
||||
children::Vector{Node}
|
||||
id::Base.UUID
|
||||
|
||||
nodeReduction::Union{Operation, Missing}
|
||||
nodeSplit::Union{Operation, Missing}
|
||||
|
||||
# for ComputeTasks there can be multiple fusions, unlike the DataTasks
|
||||
nodeFusions::Vector{Operation}
|
||||
end
|
||||
|
||||
DataTaskNode(t::AbstractDataTask) = DataTaskNode(t, Vector{Node}(), Vector{Node}(), UUIDs.uuid1(rng[threadid()]), missing, missing, missing)
|
||||
ComputeTaskNode(t::AbstractComputeTask) = ComputeTaskNode(t, Vector{Node}(), Vector{Node}(), UUIDs.uuid1(rng[threadid()]), missing, missing, Vector{NodeFusion}())
|
||||
|
||||
struct Edge
|
||||
# edge points from child to parent
|
||||
edge::Union{Tuple{DataTaskNode, ComputeTaskNode}, Tuple{ComputeTaskNode, DataTaskNode}}
|
||||
end
|
@ -1,6 +1,8 @@
|
||||
# functions that apply graph operations
|
||||
"""
|
||||
apply_all!(graph::DAG)
|
||||
|
||||
# applies all unapplied operations in the DAG
|
||||
Apply all unapplied operations in the DAG. Is automatically called in all functions that require the latest state of the [`DAG`](@ref).
|
||||
"""
|
||||
function apply_all!(graph::DAG)
|
||||
while !isempty(graph.operationsToApply)
|
||||
# get next operation to apply from front of the deque
|
||||
@ -15,46 +17,100 @@ function apply_all!(graph::DAG)
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
apply_operation!(graph::DAG, operation::Operation)
|
||||
|
||||
Fallback implementation of apply_operation! for unimplemented operation types, throwing an error.
|
||||
"""
|
||||
function apply_operation!(graph::DAG, operation::Operation)
|
||||
error("Unknown operation type!")
|
||||
return error("Unknown operation type!")
|
||||
end
|
||||
|
||||
"""
|
||||
apply_operation!(graph::DAG, operation::NodeFusion)
|
||||
|
||||
Apply the given [`NodeFusion`](@ref) to the graph. Generic wrapper around [`node_fusion!`](@ref).
|
||||
|
||||
Return an [`AppliedNodeFusion`](@ref) object generated from the graph's [`Diff`](@ref).
|
||||
"""
|
||||
function apply_operation!(graph::DAG, operation::NodeFusion)
|
||||
diff = node_fusion!(graph, operation.input[1], operation.input[2], operation.input[3])
|
||||
diff = node_fusion!(
|
||||
graph,
|
||||
operation.input[1],
|
||||
operation.input[2],
|
||||
operation.input[3],
|
||||
)
|
||||
return AppliedNodeFusion(operation, diff)
|
||||
end
|
||||
|
||||
"""
|
||||
apply_operation!(graph::DAG, operation::NodeReduction)
|
||||
|
||||
Apply the given [`NodeReduction`](@ref) to the graph. Generic wrapper around [`node_reduction!`](@ref).
|
||||
|
||||
Return an [`AppliedNodeReduction`](@ref) object generated from the graph's [`Diff`](@ref).
|
||||
"""
|
||||
function apply_operation!(graph::DAG, operation::NodeReduction)
|
||||
diff = node_reduction!(graph, operation.input)
|
||||
return AppliedNodeReduction(operation, diff)
|
||||
end
|
||||
|
||||
"""
|
||||
apply_operation!(graph::DAG, operation::NodeSplit)
|
||||
|
||||
Apply the given [`NodeSplit`](@ref) to the graph. Generic wrapper around [`node_split!`](@ref).
|
||||
|
||||
Return an [`AppliedNodeSplit`](@ref) object generated from the graph's [`Diff`](@ref).
|
||||
"""
|
||||
function apply_operation!(graph::DAG, operation::NodeSplit)
|
||||
diff = node_split!(graph, operation.input)
|
||||
return AppliedNodeSplit(operation, diff)
|
||||
end
|
||||
|
||||
"""
|
||||
revert_operation!(graph::DAG, operation::AppliedOperation)
|
||||
|
||||
Fallback implementation of operation reversion for unimplemented operation types, throwing an error.
|
||||
"""
|
||||
function revert_operation!(graph::DAG, operation::AppliedOperation)
|
||||
error("Unknown operation type!")
|
||||
return error("Unknown operation type!")
|
||||
end
|
||||
|
||||
"""
|
||||
revert_operation!(graph::DAG, operation::AppliedNodeFusion)
|
||||
|
||||
Revert the applied node fusion on the graph. Return the original [`NodeFusion`](@ref) operation.
|
||||
"""
|
||||
function revert_operation!(graph::DAG, operation::AppliedNodeFusion)
|
||||
revert_diff!(graph, operation.diff)
|
||||
return operation.operation
|
||||
end
|
||||
|
||||
"""
|
||||
revert_operation!(graph::DAG, operation::AppliedNodeReduction)
|
||||
|
||||
Revert the applied node fusion on the graph. Return the original [`NodeReduction`](@ref) operation.
|
||||
"""
|
||||
function revert_operation!(graph::DAG, operation::AppliedNodeReduction)
|
||||
revert_diff!(graph, operation.diff)
|
||||
return operation.operation
|
||||
end
|
||||
|
||||
"""
|
||||
revert_operation!(graph::DAG, operation::AppliedNodeSplit)
|
||||
|
||||
Revert the applied node fusion on the graph. Return the original [`NodeSplit`](@ref) operation.
|
||||
"""
|
||||
function revert_operation!(graph::DAG, operation::AppliedNodeSplit)
|
||||
revert_diff!(graph, operation.diff)
|
||||
return operation.operation
|
||||
end
|
||||
|
||||
"""
|
||||
revert_diff!(graph::DAG, diff::Diff)
|
||||
|
||||
Revert the given diff on the graph. Used to revert the individual [`AppliedOperation`](@ref)s with [`revert_operation!`](@ref).
|
||||
"""
|
||||
function revert_diff!(graph::DAG, diff::Diff)
|
||||
# add removed nodes, remove added nodes, same for edges
|
||||
# note the order
|
||||
@ -71,10 +127,22 @@ function revert_diff!(graph::DAG, diff::Diff)
|
||||
for edge in diff.removedEdges
|
||||
insert_edge!(graph, edge.edge[1], edge.edge[2], false)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Fuse nodes n1 -> n2 -> n3 together into one node, return the applied difference to the graph
|
||||
function node_fusion!(graph::DAG, n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
|
||||
"""
|
||||
node_fusion!(graph::DAG, n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
|
||||
|
||||
Fuse nodes n1 -> n2 -> n3 together into one node, return the applied difference to the graph.
|
||||
|
||||
For details see [`NodeFusion`](@ref).
|
||||
"""
|
||||
function node_fusion!(
|
||||
graph::DAG,
|
||||
n1::ComputeTaskNode,
|
||||
n2::DataTaskNode,
|
||||
n3::ComputeTaskNode,
|
||||
)
|
||||
# @assert is_valid_node_fusion_input(graph, n1, n2, n3)
|
||||
|
||||
# clear snapshot
|
||||
@ -97,7 +165,8 @@ function node_fusion!(graph::DAG, n1::ComputeTaskNode, n2::DataTaskNode, n3::Com
|
||||
remove_node!(graph, n3)
|
||||
|
||||
# create new node with the fused compute task
|
||||
new_node = ComputeTaskNode(FusedComputeTask{typeof(n1.task),typeof(n3.task)}())
|
||||
new_node =
|
||||
ComputeTaskNode(FusedComputeTask{typeof(n1.task), typeof(n3.task)}())
|
||||
insert_node!(graph, new_node)
|
||||
|
||||
# use a set for combined children of n1 and n3 to not get duplicates
|
||||
@ -128,6 +197,13 @@ function node_fusion!(graph::DAG, n1::ComputeTaskNode, n2::DataTaskNode, n3::Com
|
||||
return get_snapshot_diff(graph)
|
||||
end
|
||||
|
||||
"""
|
||||
node_reduction!(graph::DAG, nodes::Vector{Node})
|
||||
|
||||
Reduce the given nodes together into one node, return the applied difference to the graph.
|
||||
|
||||
For details see [`NodeReduction`](@ref).
|
||||
"""
|
||||
function node_reduction!(graph::DAG, nodes::Vector{Node})
|
||||
# @assert is_valid_node_reduction_input(graph, nodes)
|
||||
|
||||
@ -136,7 +212,7 @@ function node_reduction!(graph::DAG, nodes::Vector{Node})
|
||||
|
||||
n1 = nodes[1]
|
||||
n1_children = children(n1)
|
||||
|
||||
|
||||
n1_parents = Set(n1.parents)
|
||||
new_parents = Set{Node}()
|
||||
|
||||
@ -167,6 +243,13 @@ function node_reduction!(graph::DAG, nodes::Vector{Node})
|
||||
return get_snapshot_diff(graph)
|
||||
end
|
||||
|
||||
"""
|
||||
node_split!(graph::DAG, n1::Node)
|
||||
|
||||
Split the given node into one node per parent, return the applied difference to the graph.
|
||||
|
||||
For details see [`NodeSplit`](@ref).
|
||||
"""
|
||||
function node_split!(graph::DAG, n1::Node)
|
||||
# @assert is_valid_node_split_input(graph, n1)
|
||||
|
140
src/operation/clean.jl
Normal file
140
src/operation/clean.jl
Normal file
@ -0,0 +1,140 @@
|
||||
# These are functions for "cleaning" nodes, i.e. regenerating the possible operations for a node
|
||||
|
||||
"""
|
||||
find_fusions!(graph::DAG, node::DataTaskNode)
|
||||
|
||||
Find node fusions involving the given data node. The function pushes the found [`NodeFusion`](@ref) (if any) everywhere it needs to be and returns nothing.
|
||||
|
||||
Does nothing if the node already has a node fusion set. Since it's a data node, only one node fusion can be possible with it.
|
||||
"""
|
||||
function find_fusions!(graph::DAG, node::DataTaskNode)
|
||||
# if there is already a fusion here, skip to avoid duplicates
|
||||
if !ismissing(node.nodeFusion)
|
||||
return nothing
|
||||
end
|
||||
|
||||
if length(node.parents) != 1 || length(node.children) != 1
|
||||
return nothing
|
||||
end
|
||||
|
||||
child_node = first(node.children)
|
||||
parent_node = first(node.parents)
|
||||
|
||||
if !(child_node in graph) || !(parent_node in graph)
|
||||
error("Parents/Children that are not in the graph!!!")
|
||||
end
|
||||
|
||||
if length(child_node.parents) != 1
|
||||
return nothing
|
||||
end
|
||||
|
||||
nf = NodeFusion((child_node, node, parent_node))
|
||||
push!(graph.possibleOperations.nodeFusions, nf)
|
||||
push!(child_node.nodeFusions, nf)
|
||||
node.nodeFusion = nf
|
||||
push!(parent_node.nodeFusions, nf)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
find_fusions!(graph::DAG, node::ComputeTaskNode)
|
||||
|
||||
Find node fusions involving the given compute node. The function pushes the found [`NodeFusion`](@ref)s (if any) everywhere they need to be and returns nothing.
|
||||
"""
|
||||
function find_fusions!(graph::DAG, node::ComputeTaskNode)
|
||||
# just find fusions in neighbouring DataTaskNodes
|
||||
for child in node.children
|
||||
find_fusions!(graph, child)
|
||||
end
|
||||
|
||||
for parent in node.parents
|
||||
find_fusions!(graph, parent)
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
find_reductions!(graph::DAG, node::Node)
|
||||
|
||||
Find node reductions involving the given node. The function pushes the found [`NodeReduction`](@ref) (if any) everywhere it needs to be and returns nothing.
|
||||
"""
|
||||
function find_reductions!(graph::DAG, node::Node)
|
||||
# there can only be one reduction per node, avoid adding duplicates
|
||||
if !ismissing(node.nodeReduction)
|
||||
return nothing
|
||||
end
|
||||
|
||||
reductionVector = nothing
|
||||
# possible reductions are with nodes that are partners, i.e. parents of children
|
||||
partners_ = partners(node)
|
||||
delete!(partners_, node)
|
||||
for partner in partners_
|
||||
if partner ∉ graph.nodes
|
||||
error("Partner is not part of the graph")
|
||||
end
|
||||
|
||||
if can_reduce(node, partner)
|
||||
if Set(node.children) != Set(partner.children)
|
||||
error("Not equal children")
|
||||
end
|
||||
if reductionVector === nothing
|
||||
# only when there's at least one reduction partner, insert the vector
|
||||
reductionVector = Vector{Node}()
|
||||
push!(reductionVector, node)
|
||||
end
|
||||
|
||||
push!(reductionVector, partner)
|
||||
end
|
||||
end
|
||||
|
||||
if reductionVector !== nothing
|
||||
nr = NodeReduction(reductionVector)
|
||||
push!(graph.possibleOperations.nodeReductions, nr)
|
||||
for node in reductionVector
|
||||
if !ismissing(node.nodeReduction)
|
||||
# it can happen that the dirty node becomes part of an existing NodeReduction and overrides those ones now
|
||||
# this is only a problem insofar the existing NodeReduction has to be deleted and replaced also in the possibleOperations
|
||||
invalidate_caches!(graph, node.nodeReduction)
|
||||
end
|
||||
node.nodeReduction = nr
|
||||
end
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
find_splits!(graph::DAG, node::Node)
|
||||
|
||||
Find the node split of the given node. The function pushes the found [`NodeSplit`](@ref) (if any) everywhere it needs to be and returns nothing.
|
||||
"""
|
||||
function find_splits!(graph::DAG, node::Node)
|
||||
if !ismissing(node.nodeSplit)
|
||||
return nothing
|
||||
end
|
||||
|
||||
if (can_split(node))
|
||||
ns = NodeSplit(node)
|
||||
push!(graph.possibleOperations.nodeSplits, ns)
|
||||
node.nodeSplit = ns
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
clean_node!(graph::DAG, node::Node)
|
||||
|
||||
Sort this node's parent and child sets, then find fusions, reductions and splits involving it. Needs to be called after the node was changed in some way.
|
||||
"""
|
||||
function clean_node!(graph::DAG, node::Node)
|
||||
sort_node!(node)
|
||||
|
||||
find_fusions!(graph, node)
|
||||
find_reductions!(graph, node)
|
||||
find_splits!(graph, node)
|
||||
|
||||
return nothing
|
||||
end
|
264
src/operation/find.jl
Normal file
264
src/operation/find.jl
Normal file
@ -0,0 +1,264 @@
|
||||
# functions that find operations on the inital graph
|
||||
|
||||
using Base.Threads
|
||||
|
||||
"""
|
||||
insert_operation!(nf::NodeFusion, locks::Dict{ComputeTaskNode, SpinLock})
|
||||
|
||||
Insert the given node fusion into its input nodes' operation caches. For the compute nodes, locking via the given `locks` is employed to have safe multi-threading. For a large set of nodes, contention on the locks should be very small.
|
||||
"""
|
||||
function insert_operation!(
|
||||
nf::NodeFusion,
|
||||
locks::Dict{ComputeTaskNode, SpinLock},
|
||||
)
|
||||
n1 = nf.input[1]
|
||||
n2 = nf.input[2]
|
||||
n3 = nf.input[3]
|
||||
|
||||
lock(locks[n1]) do
|
||||
return push!(nf.input[1].nodeFusions, nf)
|
||||
end
|
||||
n2.nodeFusion = nf
|
||||
lock(locks[n3]) do
|
||||
return push!(nf.input[3].nodeFusions, nf)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
insert_operation!(nf::NodeReduction)
|
||||
|
||||
Insert the given node reduction into its input nodes' operation caches. This is thread-safe.
|
||||
"""
|
||||
function insert_operation!(nr::NodeReduction)
|
||||
for n in nr.input
|
||||
n.nodeReduction = nr
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
insert_operation!(nf::NodeSplit)
|
||||
|
||||
Insert the given node split into its input node's operation cache. This is thread-safe.
|
||||
"""
|
||||
function insert_operation!(ns::NodeSplit)
|
||||
ns.input.nodeSplit = ns
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
nr_insertion!(operations::PossibleOperations, nodeReductions::Vector{Vector{NodeReduction}})
|
||||
|
||||
Insert the node reductions into the graph and the nodes' caches. Employs multithreading for speedup.
|
||||
"""
|
||||
function nr_insertion!(
|
||||
operations::PossibleOperations,
|
||||
nodeReductions::Vector{Vector{NodeReduction}},
|
||||
)
|
||||
total_len = 0
|
||||
for vec in nodeReductions
|
||||
total_len += length(vec)
|
||||
end
|
||||
sizehint!(operations.nodeReductions, total_len)
|
||||
|
||||
t = @task for vec in nodeReductions
|
||||
union!(operations.nodeReductions, Set(vec))
|
||||
end
|
||||
schedule(t)
|
||||
|
||||
@threads for vec in nodeReductions
|
||||
for op in vec
|
||||
insert_operation!(op)
|
||||
end
|
||||
end
|
||||
|
||||
wait(t)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
nf_insertion!(graph::DAG, operations::PossibleOperations, nodeFusions::Vector{Vector{NodeFusion}})
|
||||
|
||||
Insert the node fusions into the graph and the nodes' caches. Employs multithreading for speedup.
|
||||
"""
|
||||
function nf_insertion!(
|
||||
graph::DAG,
|
||||
operations::PossibleOperations,
|
||||
nodeFusions::Vector{Vector{NodeFusion}},
|
||||
)
|
||||
total_len = 0
|
||||
for vec in nodeFusions
|
||||
total_len += length(vec)
|
||||
end
|
||||
sizehint!(operations.nodeFusions, total_len)
|
||||
|
||||
t = @task for vec in nodeFusions
|
||||
union!(operations.nodeFusions, Set(vec))
|
||||
end
|
||||
schedule(t)
|
||||
|
||||
locks = Dict{ComputeTaskNode, SpinLock}()
|
||||
for n in graph.nodes
|
||||
if (typeof(n) <: ComputeTaskNode)
|
||||
locks[n] = SpinLock()
|
||||
end
|
||||
end
|
||||
|
||||
@threads for vec in nodeFusions
|
||||
for op in vec
|
||||
insert_operation!(op, locks)
|
||||
end
|
||||
end
|
||||
|
||||
wait(t)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
ns_insertion!(operations::PossibleOperations, nodeSplits::Vector{Vector{NodeSplits}})
|
||||
|
||||
Insert the node splits into the graph and the nodes' caches. Employs multithreading for speedup.
|
||||
"""
|
||||
function ns_insertion!(
|
||||
operations::PossibleOperations,
|
||||
nodeSplits::Vector{Vector{NodeSplit}},
|
||||
)
|
||||
total_len = 0
|
||||
for vec in nodeSplits
|
||||
total_len += length(vec)
|
||||
end
|
||||
sizehint!(operations.nodeSplits, total_len)
|
||||
|
||||
t = @task for vec in nodeSplits
|
||||
union!(operations.nodeSplits, Set(vec))
|
||||
end
|
||||
schedule(t)
|
||||
|
||||
@threads for vec in nodeSplits
|
||||
for op in vec
|
||||
insert_operation!(op)
|
||||
end
|
||||
end
|
||||
|
||||
wait(t)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
generate_operations(graph::DAG)
|
||||
|
||||
Generate all possible operations on the graph. Used initially when the graph is freshly assembled or parsed. Uses multithreading for speedup.
|
||||
|
||||
Safely inserts all the found operations into the graph and its nodes.
|
||||
"""
|
||||
function generate_operations(graph::DAG)
|
||||
generatedFusions = [Vector{NodeFusion}() for _ in 1:nthreads()]
|
||||
generatedReductions = [Vector{NodeReduction}() for _ in 1:nthreads()]
|
||||
generatedSplits = [Vector{NodeSplit}() for _ in 1:nthreads()]
|
||||
|
||||
# make sure the graph is fully generated through
|
||||
apply_all!(graph)
|
||||
|
||||
nodeArray = collect(graph.nodes)
|
||||
|
||||
# sort all nodes
|
||||
@threads for node in nodeArray
|
||||
sort_node!(node)
|
||||
end
|
||||
|
||||
checkedNodes = Set{Node}()
|
||||
checkedNodesLock = SpinLock()
|
||||
# --- find possible node reductions ---
|
||||
@threads for node in nodeArray
|
||||
# we're looking for nodes with multiple parents, those parents can then potentially reduce with one another
|
||||
if (length(node.parents) <= 1)
|
||||
continue
|
||||
end
|
||||
|
||||
candidates = node.parents
|
||||
|
||||
# sort into equivalence classes
|
||||
trie = NodeTrie()
|
||||
|
||||
for candidate in candidates
|
||||
# insert into trie
|
||||
insert!(trie, candidate)
|
||||
end
|
||||
|
||||
nodeReductions = collect(trie)
|
||||
|
||||
for nrVec in nodeReductions
|
||||
# parent sets are ordered and any node can only be part of one nodeReduction, so a NodeReduction is uniquely identifiable by its first element
|
||||
# this prevents duplicate nodeReductions being generated
|
||||
lock(checkedNodesLock)
|
||||
if (nrVec[1] in checkedNodes)
|
||||
unlock(checkedNodesLock)
|
||||
continue
|
||||
else
|
||||
push!(checkedNodes, nrVec[1])
|
||||
end
|
||||
unlock(checkedNodesLock)
|
||||
|
||||
push!(generatedReductions[threadid()], NodeReduction(nrVec))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# launch thread for node reduction insertion
|
||||
# remove duplicates
|
||||
nr_task = @task nr_insertion!(graph.possibleOperations, generatedReductions)
|
||||
schedule(nr_task)
|
||||
|
||||
# --- find possible node fusions ---
|
||||
@threads for node in nodeArray
|
||||
if (typeof(node) <: DataTaskNode)
|
||||
if length(node.parents) != 1
|
||||
# data node can only have a single parent
|
||||
continue
|
||||
end
|
||||
parent_node = first(node.parents)
|
||||
|
||||
if length(node.children) != 1
|
||||
# this node is an entry node or has multiple children which should not be possible
|
||||
continue
|
||||
end
|
||||
child_node = first(node.children)
|
||||
if (length(child_node.parents) != 1)
|
||||
continue
|
||||
end
|
||||
|
||||
push!(
|
||||
generatedFusions[threadid()],
|
||||
NodeFusion((child_node, node, parent_node)),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# launch thread for node fusion insertion
|
||||
nf_task =
|
||||
@task nf_insertion!(graph, graph.possibleOperations, generatedFusions)
|
||||
schedule(nf_task)
|
||||
|
||||
# find possible node splits
|
||||
@threads for node in nodeArray
|
||||
if (can_split(node))
|
||||
push!(generatedSplits[threadid()], NodeSplit(node))
|
||||
end
|
||||
end
|
||||
|
||||
# launch thread for node split insertion
|
||||
ns_task = @task ns_insertion!(graph.possibleOperations, generatedSplits)
|
||||
schedule(ns_task)
|
||||
|
||||
empty!(graph.dirtyNodes)
|
||||
|
||||
wait(nr_task)
|
||||
wait(nf_task)
|
||||
wait(ns_task)
|
||||
|
||||
return nothing
|
||||
end
|
23
src/operation/get.jl
Normal file
23
src/operation/get.jl
Normal file
@ -0,0 +1,23 @@
|
||||
# function to return the possible operations of a graph
|
||||
|
||||
using Base.Threads
|
||||
|
||||
"""
|
||||
get_operations(graph::DAG)
|
||||
|
||||
Return the [`PossibleOperations`](@ref) of the graph at the current state.
|
||||
"""
|
||||
function get_operations(graph::DAG)
|
||||
apply_all!(graph)
|
||||
|
||||
if isempty(graph.possibleOperations)
|
||||
generate_operations(graph)
|
||||
end
|
||||
|
||||
for node in graph.dirtyNodes
|
||||
clean_node!(graph, node)
|
||||
end
|
||||
empty!(graph.dirtyNodes)
|
||||
|
||||
return graph.possibleOperations
|
||||
end
|
@ -1,3 +1,8 @@
|
||||
"""
|
||||
show(io::IO, ops::PossibleOperations)
|
||||
|
||||
Print a string representation of the set of possible operations to io.
|
||||
"""
|
||||
function show(io::IO, ops::PossibleOperations)
|
||||
print(io, length(ops.nodeFusions))
|
||||
println(io, " Node Fusions: ")
|
||||
@ -16,23 +21,38 @@ function show(io::IO, ops::PossibleOperations)
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
show(io::IO, op::NodeReduction)
|
||||
|
||||
Print a string representation of the node reduction to io.
|
||||
"""
|
||||
function show(io::IO, op::NodeReduction)
|
||||
print(io, "NR: ")
|
||||
print(io, length(op.input))
|
||||
print(io, "x")
|
||||
print(io, op.input[1].task)
|
||||
return print(io, op.input[1].task)
|
||||
end
|
||||
|
||||
"""
|
||||
show(io::IO, op::NodeSplit)
|
||||
|
||||
Print a string representation of the node split to io.
|
||||
"""
|
||||
function show(io::IO, op::NodeSplit)
|
||||
print(io, "NS: ")
|
||||
print(io, op.input.task)
|
||||
return print(io, op.input.task)
|
||||
end
|
||||
|
||||
"""
|
||||
show(io::IO, op::NodeFusion)
|
||||
|
||||
Print a string representation of the node fusion to io.
|
||||
"""
|
||||
function show(io::IO, op::NodeFusion)
|
||||
print(io, "NF: ")
|
||||
print(io, op.input[1].task)
|
||||
print(io, "->")
|
||||
print(io, op.input[2].task)
|
||||
print(io, "->")
|
||||
print(io, op.input[3].task)
|
||||
return print(io, op.input[3].task)
|
||||
end
|
117
src/operation/type.jl
Normal file
117
src/operation/type.jl
Normal file
@ -0,0 +1,117 @@
|
||||
"""
|
||||
Operation
|
||||
|
||||
An abstract base class for operations. An operation can be applied to a [`DAG`](@ref), changing its nodes and edges.
|
||||
|
||||
Possible operations on a [`DAG`](@ref) can be retrieved using [`get_operations`](@ref).
|
||||
|
||||
See also: [`push_operation!`](@ref), [`pop_operation!`](@ref)
|
||||
"""
|
||||
abstract type Operation end
|
||||
|
||||
"""
|
||||
AppliedOperation
|
||||
|
||||
An abstract base class for already applied operations.
|
||||
An applied operation can be reversed iff it is the last applied operation on the DAG.
|
||||
Every applied operation stores a [`Diff`](@ref) from when it was initially applied to be able to revert the operation.
|
||||
|
||||
See also: [`revert_operation!`](@ref).
|
||||
"""
|
||||
abstract type AppliedOperation end
|
||||
|
||||
"""
|
||||
NodeFusion <: Operation
|
||||
|
||||
The NodeFusion operation. Represents the fusing of a chain of compute node -> data node -> compute node.
|
||||
|
||||
After the node fusion is applied, the graph has 2 fewer nodes and edges, and a new [`FusedComputeTask`](@ref) with the two input compute nodes as parts.
|
||||
|
||||
# Requirements for successful application
|
||||
|
||||
A chain of (n1, n2, n3) can be fused if:
|
||||
- All nodes are in the graph.
|
||||
- (n1, n2) is an edge in the graph.
|
||||
- (n2, n3) is an edge in the graph.
|
||||
- n2 has exactly one parent (n3) and exactly one child (n1).
|
||||
- n1 has exactly one parent (n2).
|
||||
|
||||
[`is_valid_node_fusion_input`](@ref) can be used to `@assert` these requirements.
|
||||
|
||||
See also: [`can_fuse`](@ref)
|
||||
"""
|
||||
struct NodeFusion <: Operation
|
||||
input::Tuple{ComputeTaskNode, DataTaskNode, ComputeTaskNode}
|
||||
end
|
||||
|
||||
"""
|
||||
AppliedNodeFusion <: AppliedOperation
|
||||
|
||||
The applied version of the [`NodeFusion`](@ref).
|
||||
"""
|
||||
struct AppliedNodeFusion <: AppliedOperation
|
||||
operation::NodeFusion
|
||||
diff::Diff
|
||||
end
|
||||
|
||||
"""
|
||||
NodeReduction <: Operation
|
||||
|
||||
The NodeReduction operation. Represents the reduction of two or more nodes with one another.
|
||||
Only one of the input nodes is kept, while all others are deleted and their parents are accumulated in the kept node's parents instead.
|
||||
|
||||
After the node reduction is applied, the graph has `length(nr.input) - 1` fewer nodes.
|
||||
|
||||
# Requirements for successful application
|
||||
|
||||
A vector of nodes can be reduced if:
|
||||
- All nodes are in the graph.
|
||||
- All nodes have the same task type.
|
||||
- All nodes have the same set of children.
|
||||
|
||||
[`is_valid_node_reduction_input`](@ref) can be used to `@assert` these requirements.
|
||||
|
||||
See also: [`can_reduce`](@ref)
|
||||
"""
|
||||
struct NodeReduction <: Operation
|
||||
input::Vector{Node}
|
||||
end
|
||||
|
||||
"""
|
||||
AppliedNodeReduction <: AppliedOperation
|
||||
|
||||
The applied version of the [`NodeReduction`](@ref).
|
||||
"""
|
||||
struct AppliedNodeReduction <: AppliedOperation
|
||||
operation::NodeReduction
|
||||
diff::Diff
|
||||
end
|
||||
|
||||
"""
|
||||
NodeSplit <: Operation
|
||||
|
||||
The NodeSplit operation. Represents the split of its input node into one node for each of its parents. It is the reverse operation to the [`NodeReduction`](@ref).
|
||||
|
||||
# Requirements for successful application
|
||||
|
||||
A node can be split if:
|
||||
- It is in the graph.
|
||||
- It has at least 2 parents.
|
||||
|
||||
[`is_valid_node_split_input`](@ref) can be used to `@assert` these requirements.
|
||||
|
||||
See also: [`can_split`](@ref)
|
||||
"""
|
||||
struct NodeSplit <: Operation
|
||||
input::Node
|
||||
end
|
||||
|
||||
"""
|
||||
AppliedNodeSplit <: AppliedOperation
|
||||
|
||||
The applied version of the [`NodeSplit`](@ref).
|
||||
"""
|
||||
struct AppliedNodeSplit <: AppliedOperation
|
||||
operation::NodeSplit
|
||||
diff::Diff
|
||||
end
|
167
src/operation/utility.jl
Normal file
167
src/operation/utility.jl
Normal file
@ -0,0 +1,167 @@
|
||||
"""
|
||||
isempty(operations::PossibleOperations)
|
||||
|
||||
Return whether `operations` is empty, i.e. all of its fields are empty.
|
||||
"""
|
||||
function isempty(operations::PossibleOperations)
|
||||
return isempty(operations.nodeFusions) &&
|
||||
isempty(operations.nodeReductions) &&
|
||||
isempty(operations.nodeSplits)
|
||||
end
|
||||
|
||||
"""
|
||||
length(operations::PossibleOperations)
|
||||
|
||||
Return a named tuple with the number of each of the operation types as a named tuple. The fields are named the same as the [`PossibleOperations`](@ref)'.
|
||||
"""
|
||||
function length(operations::PossibleOperations)
|
||||
return (
|
||||
nodeFusions = length(operations.nodeFusions),
|
||||
nodeReductions = length(operations.nodeReductions),
|
||||
nodeSplits = length(operations.nodeSplits),
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
delete!(operations::PossibleOperations, op::NodeFusion)
|
||||
|
||||
Delete the given node fusion from the possible operations.
|
||||
"""
|
||||
function delete!(operations::PossibleOperations, op::NodeFusion)
|
||||
delete!(operations.nodeFusions, op)
|
||||
return operations
|
||||
end
|
||||
|
||||
"""
|
||||
delete!(operations::PossibleOperations, op::NodeReduction)
|
||||
|
||||
Delete the given node reduction from the possible operations.
|
||||
"""
|
||||
function delete!(operations::PossibleOperations, op::NodeReduction)
|
||||
delete!(operations.nodeReductions, op)
|
||||
return operations
|
||||
end
|
||||
|
||||
"""
|
||||
delete!(operations::PossibleOperations, op::NodeSplit)
|
||||
|
||||
Delete the given node split from the possible operations.
|
||||
"""
|
||||
function delete!(operations::PossibleOperations, op::NodeSplit)
|
||||
delete!(operations.nodeSplits, op)
|
||||
return operations
|
||||
end
|
||||
|
||||
"""
|
||||
can_fuse(n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
|
||||
|
||||
Return whether the given nodes can be fused. See [`NodeFusion`](@ref) for the requirements.
|
||||
"""
|
||||
function can_fuse(n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
|
||||
if !is_child(n1, n2) || !is_child(n2, n3)
|
||||
# the checks are redundant but maybe a good sanity check
|
||||
return false
|
||||
end
|
||||
|
||||
if length(n2.parents) != 1 ||
|
||||
length(n2.children) != 1 ||
|
||||
length(n1.parents) != 1
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
"""
|
||||
can_reduce(n1::Node, n2::Node)
|
||||
|
||||
Return whether the given two nodes can be reduced. See [`NodeReduction`](@ref) for the requirements.
|
||||
"""
|
||||
function can_reduce(n1::Node, n2::Node)
|
||||
if (n1.task != n2.task)
|
||||
return false
|
||||
end
|
||||
|
||||
n1_length = length(n1.children)
|
||||
n2_length = length(n2.children)
|
||||
|
||||
if (n1_length != n2_length)
|
||||
return false
|
||||
end
|
||||
|
||||
# this seems to be the most common case so do this first
|
||||
# doing it manually is a lot faster than using the sets for a general solution
|
||||
if (n1_length == 2)
|
||||
if (n1.children[1] != n2.children[1])
|
||||
if (n1.children[1] != n2.children[2])
|
||||
return false
|
||||
end
|
||||
# 1_1 == 2_2
|
||||
if (n1.children[2] != n2.children[1])
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
# 1_1 == 2_1
|
||||
if (n1.children[2] != n2.children[2])
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
# this is simple
|
||||
if (n1_length == 1)
|
||||
return n1.children[1] == n2.children[1]
|
||||
end
|
||||
|
||||
# this takes a long time
|
||||
return Set(n1.children) == Set(n2.children)
|
||||
end
|
||||
|
||||
"""
|
||||
can_split(n1::Node)
|
||||
|
||||
Return whether the given node can be split. See [`NodeSplit`](@ref) for the requirements.
|
||||
"""
|
||||
function can_split(n::Node)
|
||||
return length(parents(n)) > 1
|
||||
end
|
||||
|
||||
"""
|
||||
==(op1::Operation, op2::Operation)
|
||||
|
||||
Fallback implementation of operation equality. Return false. Actual comparisons are done by the overloads of same type operation comparisons.
|
||||
"""
|
||||
function ==(op1::Operation, op2::Operation)
|
||||
return false
|
||||
end
|
||||
|
||||
"""
|
||||
==(op1::NodeFusion, op2::NodeFusion)
|
||||
|
||||
Equality comparison between two node fusions. Two node fusions are considered equal if they have the same inputs.
|
||||
"""
|
||||
function ==(op1::NodeFusion, op2::NodeFusion)
|
||||
# there can only be one node fusion on a given data task, so if the data task is the same, the fusion is the same
|
||||
return op1.input[2] == op2.input[2]
|
||||
end
|
||||
|
||||
"""
|
||||
==(op1::NodeReduction, op2::NodeReduction)
|
||||
|
||||
Equality comparison between two node reductions. Two node reductions are considered equal when they have the same inputs.
|
||||
"""
|
||||
function ==(op1::NodeReduction, op2::NodeReduction)
|
||||
# node reductions are equal exactly if their first input is the same
|
||||
return op1.input[1].id == op2.input[1].id
|
||||
end
|
||||
|
||||
"""
|
||||
==(op1::NodeSplit, op2::NodeSplit)
|
||||
|
||||
Equality comparison between two node splits. Two node splits are considered equal if they have the same input node.
|
||||
"""
|
||||
function ==(op1::NodeSplit, op2::NodeSplit)
|
||||
return op1.input == op2.input
|
||||
end
|
174
src/operation/validate.jl
Normal file
174
src/operation/validate.jl
Normal file
@ -0,0 +1,174 @@
|
||||
# functions to throw assertion errors for inconsistent or wrong node operations
|
||||
# should be called with @assert
|
||||
# the functions throw their own errors though, to still have helpful error messages
|
||||
|
||||
"""
|
||||
is_valid_node_fusion_input(graph::DAG, n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
|
||||
|
||||
Assert for a gven node fusion input whether the nodes can be fused. For the requirements of a node fusion see [`NodeFusion`](@ref).
|
||||
|
||||
Intended for use with `@assert` or `@test`.
|
||||
"""
|
||||
function is_valid_node_fusion_input(
|
||||
graph::DAG,
|
||||
n1::ComputeTaskNode,
|
||||
n2::DataTaskNode,
|
||||
n3::ComputeTaskNode,
|
||||
)
|
||||
if !(n1 in graph) || !(n2 in graph) || !(n3 in graph)
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Fusion] The given nodes are not part of the given graph",
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
if !is_child(n1, n2) ||
|
||||
!is_child(n2, n3) ||
|
||||
!is_parent(n3, n2) ||
|
||||
!is_parent(n2, n1)
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Fusion] The given nodes are not connected by edges which is required for node fusion",
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
if length(n2.parents) > 1
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Fusion] The given data node has more than one parent",
|
||||
),
|
||||
)
|
||||
end
|
||||
if length(n2.children) > 1
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Fusion] The given data node has more than one child",
|
||||
),
|
||||
)
|
||||
end
|
||||
if length(n1.parents) > 1
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Fusion] The given n1 has more than one parent",
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
"""
|
||||
is_valid_node_reduction_input(graph::DAG, nodes::Vector{Node})
|
||||
|
||||
Assert for a gven node reduction input whether the nodes can be reduced. For the requirements of a node reduction see [`NodeReduction`](@ref).
|
||||
|
||||
Intended for use with `@assert` or `@test`.
|
||||
"""
|
||||
function is_valid_node_reduction_input(graph::DAG, nodes::Vector{Node})
|
||||
for n in nodes
|
||||
if n ∉ graph
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Reduction] The given nodes are not part of the given graph",
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
t = typeof(nodes[1].task)
|
||||
for n in nodes
|
||||
if typeof(n.task) != t
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Reduction] The given nodes are not of the same type",
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
n1_children = nodes[1].children
|
||||
for n in nodes
|
||||
if Set(n1_children) != Set(n.children)
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Reduction] The given nodes do not have equal prerequisite nodes which is required for node reduction",
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
"""
|
||||
is_valid_node_split_input(graph::DAG, n1::Node)
|
||||
|
||||
Assert for a gven node split input whether the node can be split. For the requirements of a node split see [`NodeSplit`](@ref).
|
||||
|
||||
Intended for use with `@assert` or `@test`.
|
||||
"""
|
||||
function is_valid_node_split_input(graph::DAG, n1::Node)
|
||||
if n1 ∉ graph
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Split] The given node is not part of the given graph",
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
if length(n1.parents) <= 1
|
||||
throw(
|
||||
AssertionError(
|
||||
"[Node Split] The given node does not have multiple parents which is required for node split",
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
"""
|
||||
is_valid(graph::DAG, nr::NodeReduction)
|
||||
|
||||
Assert for a given [`NodeReduction`](@ref) whether it is a valid operation in the graph.
|
||||
|
||||
Intended for use with `@assert` or `@test`.
|
||||
"""
|
||||
function is_valid(graph::DAG, nr::NodeReduction)
|
||||
@assert is_valid_node_reduction_input(graph, nr.input)
|
||||
@assert nr in graph.possibleOperations.nodeReductions "NodeReduction is not part of the graph's possible operations!"
|
||||
return true
|
||||
end
|
||||
|
||||
"""
|
||||
is_valid(graph::DAG, nr::NodeSplit)
|
||||
|
||||
Assert for a given [`NodeSplit`](@ref) whether it is a valid operation in the graph.
|
||||
|
||||
Intended for use with `@assert` or `@test`.
|
||||
"""
|
||||
function is_valid(graph::DAG, ns::NodeSplit)
|
||||
@assert is_valid_node_split_input(graph, ns.input)
|
||||
@assert ns in graph.possibleOperations.nodeSplits "NodeSplit is not part of the graph's possible operations!"
|
||||
return true
|
||||
end
|
||||
|
||||
"""
|
||||
is_valid(graph::DAG, nr::NodeFusion)
|
||||
|
||||
Assert for a given [`NodeFusion`](@ref) whether it is a valid operation in the graph.
|
||||
|
||||
Intended for use with `@assert` or `@test`.
|
||||
"""
|
||||
function is_valid(graph::DAG, nf::NodeFusion)
|
||||
@assert is_valid_node_fusion_input(
|
||||
graph,
|
||||
nf.input[1],
|
||||
nf.input[2],
|
||||
nf.input[3],
|
||||
)
|
||||
@assert nf in graph.possibleOperations.nodeFusions "NodeFusion is not part of the graph's possible operations!"
|
||||
return true
|
||||
end
|
@ -1,115 +0,0 @@
|
||||
# functions for "cleaning" nodes, i.e. regenerating the possible operations for a node
|
||||
|
||||
# function to find node fusions involving the given node if it's a data node
|
||||
# pushes the found fusion everywhere it needs to be and returns nothing
|
||||
function find_fusions!(graph::DAG, node::DataTaskNode)
|
||||
# if there is already a fusion here, skip
|
||||
if !ismissing(node.nodeFusion)
|
||||
return nothing
|
||||
end
|
||||
|
||||
if length(node.parents) != 1 || length(node.children) != 1
|
||||
return nothing
|
||||
end
|
||||
|
||||
child_node = first(node.children)
|
||||
parent_node = first(node.parents)
|
||||
|
||||
if !(child_node in graph) || !(parent_node in graph)
|
||||
error("Parents/Children that are not in the graph!!!")
|
||||
end
|
||||
|
||||
if length(child_node.parents) != 1
|
||||
return nothing
|
||||
end
|
||||
|
||||
nf = NodeFusion((child_node, node, parent_node))
|
||||
push!(graph.possibleOperations.nodeFusions, nf)
|
||||
push!(child_node.nodeFusions, nf)
|
||||
node.nodeFusion = nf
|
||||
push!(parent_node.nodeFusions, nf)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
|
||||
function find_fusions!(graph::DAG, node::ComputeTaskNode)
|
||||
# just find fusions in neighbouring DataTaskNodes
|
||||
for child in node.children
|
||||
find_fusions!(graph, child)
|
||||
end
|
||||
|
||||
for parent in node.parents
|
||||
find_fusions!(graph, parent)
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
function find_reductions!(graph::DAG, node::Node)
|
||||
# there can only be one reduction per node, avoid adding duplicates
|
||||
if !ismissing(node.nodeReduction)
|
||||
return nothing
|
||||
end
|
||||
|
||||
reductionVector = nothing
|
||||
# possible reductions are with nodes that are partners, i.e. parents of children
|
||||
partners_ = partners(node)
|
||||
delete!(partners_, node)
|
||||
for partner in partners_
|
||||
if partner ∉ graph.nodes
|
||||
error("Partner is not part of the graph")
|
||||
end
|
||||
|
||||
if can_reduce(node, partner)
|
||||
if Set(node.children) != Set(partner.children)
|
||||
error("Not equal children")
|
||||
end
|
||||
if reductionVector === nothing
|
||||
# only when there's at least one reduction partner, insert the vector
|
||||
reductionVector = Vector{Node}()
|
||||
push!(reductionVector, node)
|
||||
end
|
||||
|
||||
push!(reductionVector, partner)
|
||||
end
|
||||
end
|
||||
|
||||
if reductionVector !== nothing
|
||||
nr = NodeReduction(reductionVector)
|
||||
push!(graph.possibleOperations.nodeReductions, nr)
|
||||
for node in reductionVector
|
||||
if !ismissing(node.nodeReduction)
|
||||
# it can happen that the dirty node becomes part of an existing NodeReduction and overrides those ones now
|
||||
# this is only a problem insofar the existing NodeReduction has to be deleted and replaced also in the possibleOperations
|
||||
invalidate_caches!(graph, node.nodeReduction)
|
||||
end
|
||||
node.nodeReduction = nr
|
||||
end
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
function find_splits!(graph::DAG, node::Node)
|
||||
if !ismissing(node.nodeSplit)
|
||||
return nothing
|
||||
end
|
||||
|
||||
if (can_split(node))
|
||||
ns = NodeSplit(node)
|
||||
push!(graph.possibleOperations.nodeSplits, ns)
|
||||
node.nodeSplit = ns
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# "clean" the operations on a dirty node
|
||||
function clean_node!(graph::DAG, node::Node)
|
||||
sort_node!(node)
|
||||
|
||||
find_fusions!(graph, node)
|
||||
find_reductions!(graph, node)
|
||||
find_splits!(graph, node)
|
||||
end
|
@ -1,205 +0,0 @@
|
||||
# functions that find operations on the inital graph
|
||||
|
||||
using Base.Threads
|
||||
|
||||
function insert_operation!(nf::NodeFusion, locks::Dict{ComputeTaskNode, SpinLock})
|
||||
n1 = nf.input[1]; n2 = nf.input[2]; n3 = nf.input[3]
|
||||
|
||||
lock(locks[n1]) do; push!(nf.input[1].nodeFusions, nf); end
|
||||
nf.input[2].nodeFusion = nf
|
||||
lock(locks[n3]) do; push!(nf.input[3].nodeFusions, nf); end
|
||||
return nothing
|
||||
end
|
||||
|
||||
function insert_operation!(nr::NodeReduction)
|
||||
for n in nr.input
|
||||
n.nodeReduction = nr
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
function insert_operation!(ns::NodeSplit)
|
||||
ns.input.nodeSplit = ns
|
||||
return nothing
|
||||
end
|
||||
|
||||
function nr_insertion!(operations::PossibleOperations, nodeReductions::Vector{Vector{NodeReduction}})
|
||||
total_len = 0
|
||||
for vec in nodeReductions
|
||||
total_len += length(vec)
|
||||
end
|
||||
sizehint!(operations.nodeReductions, total_len)
|
||||
|
||||
t = @task for vec in nodeReductions
|
||||
union!(operations.nodeReductions, Set(vec))
|
||||
end
|
||||
schedule(t)
|
||||
|
||||
@threads for vec in nodeReductions
|
||||
for op in vec
|
||||
insert_operation!(op)
|
||||
end
|
||||
end
|
||||
|
||||
wait(t)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
function nf_insertion!(graph::DAG, operations::PossibleOperations, nodeFusions::Vector{Vector{NodeFusion}})
|
||||
total_len = 0
|
||||
for vec in nodeFusions
|
||||
total_len += length(vec)
|
||||
end
|
||||
sizehint!(operations.nodeFusions, total_len)
|
||||
|
||||
t = @task for vec in nodeFusions
|
||||
union!(operations.nodeFusions, Set(vec))
|
||||
end
|
||||
schedule(t)
|
||||
|
||||
locks = Dict{ComputeTaskNode, SpinLock}()
|
||||
for n in graph.nodes
|
||||
if (typeof(n) <: ComputeTaskNode)
|
||||
locks[n] = SpinLock()
|
||||
end
|
||||
end
|
||||
|
||||
@threads for vec in nodeFusions
|
||||
for op in vec
|
||||
insert_operation!(op, locks)
|
||||
end
|
||||
end
|
||||
|
||||
wait(t)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
function ns_insertion!(operations::PossibleOperations, nodeSplits::Vector{Vector{NodeSplit}})
|
||||
total_len = 0
|
||||
for vec in nodeSplits
|
||||
total_len += length(vec)
|
||||
end
|
||||
sizehint!(operations.nodeSplits, total_len)
|
||||
|
||||
t = @task for vec in nodeSplits
|
||||
union!(operations.nodeSplits, Set(vec))
|
||||
end
|
||||
schedule(t)
|
||||
|
||||
@threads for vec in nodeSplits
|
||||
for op in vec
|
||||
insert_operation!(op)
|
||||
end
|
||||
end
|
||||
|
||||
wait(t)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# function to generate all possible operations on the graph
|
||||
function generate_options(graph::DAG)
|
||||
generatedFusions = [Vector{NodeFusion}() for _ in 1:nthreads()]
|
||||
generatedReductions = [Vector{NodeReduction}() for _ in 1:nthreads()]
|
||||
generatedSplits = [Vector{NodeSplit}() for _ in 1:nthreads()]
|
||||
|
||||
# make sure the graph is fully generated through
|
||||
apply_all!(graph)
|
||||
|
||||
nodeArray = collect(graph.nodes)
|
||||
|
||||
# sort all nodes
|
||||
@threads for node in nodeArray
|
||||
sort_node!(node)
|
||||
end
|
||||
|
||||
checkedNodes = Set{Node}()
|
||||
checkedNodesLock = SpinLock()
|
||||
# --- find possible node reductions ---
|
||||
@threads for node in nodeArray
|
||||
# we're looking for nodes with multiple parents, those parents can then potentially reduce with one another
|
||||
if (length(node.parents) <= 1)
|
||||
continue
|
||||
end
|
||||
|
||||
candidates = node.parents
|
||||
|
||||
# sort into equivalence classes
|
||||
trie = NodeTrie()
|
||||
|
||||
for candidate in candidates
|
||||
# insert into trie
|
||||
insert!(trie, candidate)
|
||||
end
|
||||
|
||||
nodeReductions = collect(trie)
|
||||
|
||||
for nrVec in nodeReductions
|
||||
# parent sets are ordered and any node can only be part of one nodeReduction, so a NodeReduction is uniquely identifiable by its first element
|
||||
# this prevents duplicate nodeReductions being generated
|
||||
lock(checkedNodesLock)
|
||||
if (nrVec[1] in checkedNodes)
|
||||
unlock(checkedNodesLock)
|
||||
continue
|
||||
else
|
||||
push!(checkedNodes, nrVec[1])
|
||||
end
|
||||
unlock(checkedNodesLock)
|
||||
|
||||
push!(generatedReductions[threadid()], NodeReduction(nrVec))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# launch thread for node reduction insertion
|
||||
# remove duplicates
|
||||
nr_task = @task nr_insertion!(graph.possibleOperations, generatedReductions)
|
||||
schedule(nr_task)
|
||||
|
||||
# --- find possible node fusions ---
|
||||
@threads for node in nodeArray
|
||||
if (typeof(node) <: DataTaskNode)
|
||||
if length(node.parents) != 1
|
||||
# data node can only have a single parent
|
||||
continue
|
||||
end
|
||||
parent_node = first(node.parents)
|
||||
|
||||
if length(node.children) != 1
|
||||
# this node is an entry node or has multiple children which should not be possible
|
||||
continue
|
||||
end
|
||||
child_node = first(node.children)
|
||||
if (length(child_node.parents) != 1)
|
||||
continue
|
||||
end
|
||||
|
||||
push!(generatedFusions[threadid()], NodeFusion((child_node, node, parent_node)))
|
||||
end
|
||||
end
|
||||
|
||||
# launch thread for node fusion insertion
|
||||
nf_task = @task nf_insertion!(graph, graph.possibleOperations, generatedFusions)
|
||||
schedule(nf_task)
|
||||
|
||||
# find possible node splits
|
||||
@threads for node in nodeArray
|
||||
if (can_split(node))
|
||||
push!(generatedSplits[threadid()], NodeSplit(node))
|
||||
end
|
||||
end
|
||||
|
||||
# launch thread for node split insertion
|
||||
ns_task = @task ns_insertion!(graph.possibleOperations, generatedSplits)
|
||||
schedule(ns_task)
|
||||
|
||||
empty!(graph.dirtyNodes)
|
||||
|
||||
wait(nr_task)
|
||||
wait(nf_task)
|
||||
wait(ns_task)
|
||||
|
||||
return nothing
|
||||
end
|
@ -1,18 +0,0 @@
|
||||
# function to return the possible operations of a graph
|
||||
|
||||
using Base.Threads
|
||||
|
||||
function get_operations(graph::DAG)
|
||||
apply_all!(graph)
|
||||
|
||||
if isempty(graph.possibleOperations)
|
||||
generate_options(graph)
|
||||
end
|
||||
|
||||
for node in graph.dirtyNodes
|
||||
clean_node!(graph, node)
|
||||
end
|
||||
empty!(graph.dirtyNodes)
|
||||
|
||||
return graph.possibleOperations
|
||||
end
|
@ -1,107 +0,0 @@
|
||||
|
||||
function isempty(operations::PossibleOperations)
|
||||
return isempty(operations.nodeFusions) &&
|
||||
isempty(operations.nodeReductions) &&
|
||||
isempty(operations.nodeSplits)
|
||||
end
|
||||
|
||||
function length(operations::PossibleOperations)
|
||||
return (nodeFusions = length(operations.nodeFusions),
|
||||
nodeReductions = length(operations.nodeReductions),
|
||||
nodeSplits = length(operations.nodeSplits))
|
||||
end
|
||||
|
||||
function delete!(operations::PossibleOperations, op::NodeFusion)
|
||||
delete!(operations.nodeFusions, op)
|
||||
return operations
|
||||
end
|
||||
|
||||
function delete!(operations::PossibleOperations, op::NodeReduction)
|
||||
delete!(operations.nodeReductions, op)
|
||||
return operations
|
||||
end
|
||||
|
||||
function delete!(operations::PossibleOperations, op::NodeSplit)
|
||||
delete!(operations.nodeSplits, op)
|
||||
return operations
|
||||
end
|
||||
|
||||
|
||||
function can_fuse(n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
|
||||
if !is_child(n1, n2) || !is_child(n2, n3)
|
||||
# the checks are redundant but maybe a good sanity check
|
||||
return false
|
||||
end
|
||||
|
||||
if length(n2.parents) != 1 || length(n2.children) != 1 || length(n1.parents) != 1
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function can_reduce(n1::Node, n2::Node)
|
||||
if (n1.task != n2.task)
|
||||
return false
|
||||
end
|
||||
|
||||
n1_length = length(n1.children)
|
||||
n2_length = length(n2.children)
|
||||
|
||||
if (n1_length != n2_length)
|
||||
return false
|
||||
end
|
||||
|
||||
# this seems to be the most common case so do this first
|
||||
# doing it manually is a lot faster than using the sets for a general solution
|
||||
if (n1_length == 2)
|
||||
if (n1.children[1] != n2.children[1])
|
||||
if (n1.children[1] != n2.children[2])
|
||||
return false
|
||||
end
|
||||
# 1_1 == 2_2
|
||||
if (n1.children[2] != n2.children[1])
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
# 1_1 == 2_1
|
||||
if (n1.children[2] != n2.children[2])
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
# this is simple
|
||||
if (n1_length == 1)
|
||||
return n1.children[1] == n2.children[1]
|
||||
end
|
||||
|
||||
# this takes a long time
|
||||
return Set(n1.children) == Set(n2.children)
|
||||
end
|
||||
|
||||
function can_split(n::Node)
|
||||
return length(parents(n)) > 1
|
||||
end
|
||||
|
||||
function ==(op1::Operation, op2::Operation)
|
||||
return false
|
||||
end
|
||||
|
||||
function ==(op1::NodeFusion, op2::NodeFusion)
|
||||
# there can only be one node fusion on a given data task, so if the data task is the same, the fusion is the same
|
||||
return op1.input[2] == op2.input[2]
|
||||
end
|
||||
|
||||
function ==(op1::NodeReduction, op2::NodeReduction)
|
||||
# node reductions are equal exactly if their first input is the same
|
||||
return op1.input[1].id == op2.input[1].id
|
||||
end
|
||||
|
||||
function ==(op1::NodeSplit, op2::NodeSplit)
|
||||
return op1.input == op2.input
|
||||
end
|
||||
|
||||
copy(id::UUID) = UUID(id.value)
|
@ -1,61 +0,0 @@
|
||||
# functions to throw assertion errors for inconsistent or wrong node operations
|
||||
# should be called with @assert
|
||||
# the functions throw their own errors though, to still have helpful error messages
|
||||
|
||||
function is_valid_node_fusion_input(graph::DAG, n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
|
||||
if !(n1 in graph) || !(n2 in graph) || !(n3 in graph)
|
||||
throw(AssertionError("[Node Fusion] The given nodes are not part of the given graph"))
|
||||
end
|
||||
|
||||
if !is_child(n1, n2) || !is_child(n2, n3) || !is_parent(n3, n2) || !is_parent(n2, n1)
|
||||
throw(AssertionError("[Node Fusion] The given nodes are not connected by edges which is required for node fusion"))
|
||||
end
|
||||
|
||||
if length(n2.parents) > 1
|
||||
throw(AssertionError("[Node Fusion] The given data node has more than one parent"))
|
||||
end
|
||||
if length(n2.children) > 1
|
||||
throw(AssertionError("[Node Fusion] The given data node has more than one child"))
|
||||
end
|
||||
if length(n1.parents) > 1
|
||||
throw(AssertionError("[Node Fusion] The given n1 has more than one parent"))
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function is_valid_node_reduction_input(graph::DAG, nodes::Vector{Node})
|
||||
for n in nodes
|
||||
if n ∉ graph
|
||||
throw(AssertionError("[Node Reduction] The given nodes are not part of the given graph"))
|
||||
end
|
||||
end
|
||||
|
||||
t = typeof(nodes[1].task)
|
||||
for n in nodes
|
||||
if typeof(n.task) != t
|
||||
throw(AssertionError("[Node Reduction] The given nodes are not of the same type"))
|
||||
end
|
||||
end
|
||||
|
||||
n1_children = nodes[1].children
|
||||
for n in nodes
|
||||
if Set(n1_children) != Set(n.children)
|
||||
throw(AssertionError("[Node Reduction] The given nodes do not have equal prerequisite nodes which is required for node reduction"))
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function is_valid_node_split_input(graph::DAG, n1::Node)
|
||||
if n1 ∉ graph
|
||||
throw(AssertionError("[Node Split] The given node is not part of the given graph"))
|
||||
end
|
||||
|
||||
if length(n1.parents) <= 1
|
||||
throw(AssertionError("[Node Split] The given node does not have multiple parents which is required for node split"))
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
26
src/task/compare.jl
Normal file
26
src/task/compare.jl
Normal file
@ -0,0 +1,26 @@
|
||||
"""
|
||||
==(t1::AbstractTask, t2::AbstractTask)
|
||||
|
||||
Fallback implementation of equality comparison between two abstract tasks. Always returns false. For equal specific types of t1 and t2, a more specific comparison is called instead, doing an actual comparison.
|
||||
"""
|
||||
function ==(t1::AbstractTask, t2::AbstractTask)
|
||||
return false
|
||||
end
|
||||
|
||||
"""
|
||||
==(t1::AbstractComputeTask, t2::AbstractComputeTask)
|
||||
|
||||
Equality comparison between two compute tasks.
|
||||
"""
|
||||
function ==(t1::AbstractComputeTask, t2::AbstractComputeTask)
|
||||
return typeof(t1) == typeof(t2)
|
||||
end
|
||||
|
||||
"""
|
||||
==(t1::AbstractDataTask, t2::AbstractDataTask)
|
||||
|
||||
Equality comparison between two data tasks.
|
||||
"""
|
||||
function ==(t1::AbstractDataTask, t2::AbstractDataTask)
|
||||
return data(t1) == data(t2)
|
||||
end
|
14
src/task/create.jl
Normal file
14
src/task/create.jl
Normal file
@ -0,0 +1,14 @@
|
||||
"""
|
||||
copy(t::AbstractDataTask)
|
||||
|
||||
Fallback implementation of the copy of an abstract data task, throwing an error.
|
||||
"""
|
||||
copy(t::AbstractDataTask) =
|
||||
error("Need to implement copying for your data tasks!")
|
||||
|
||||
"""
|
||||
copy(t::AbstractComputeTask)
|
||||
|
||||
Return a copy of the given compute task.
|
||||
"""
|
||||
copy(t::AbstractComputeTask) = typeof(t)()
|
9
src/task/print.jl
Normal file
9
src/task/print.jl
Normal file
@ -0,0 +1,9 @@
|
||||
"""
|
||||
show(io::IO, t::FusedComputeTask)
|
||||
|
||||
Print a string representation of the fused compute task to io.
|
||||
"""
|
||||
function show(io::IO, t::FusedComputeTask)
|
||||
(T1, T2) = get_types(t)
|
||||
return print(io, "ComputeFuse(", T1(), ", ", T2(), ")")
|
||||
end
|
75
src/task/properties.jl
Normal file
75
src/task/properties.jl
Normal file
@ -0,0 +1,75 @@
|
||||
"""
|
||||
compute(t::AbstractTask; data...)
|
||||
|
||||
Fallback implementation of the compute function of a compute task, throwing an error.
|
||||
"""
|
||||
function compute(t::AbstractTask; data...)
|
||||
return error("Need to implement compute()")
|
||||
end
|
||||
|
||||
"""
|
||||
compute_effort(t::AbstractTask)
|
||||
|
||||
Fallback implementation of the compute effort of a task, throwing an error.
|
||||
"""
|
||||
function compute_effort(t::AbstractTask)
|
||||
# default implementation using compute
|
||||
return error("Need to implement compute_effort()")
|
||||
end
|
||||
|
||||
"""
|
||||
data(t::AbstractTask)
|
||||
|
||||
Fallback implementation of the data of a task, throwing an error.
|
||||
"""
|
||||
function data(t::AbstractTask)
|
||||
return error("Need to implement data()")
|
||||
end
|
||||
|
||||
"""
|
||||
compute_effort(t::AbstractDataTask)
|
||||
|
||||
Return the compute effort of a data task, always zero, regardless of the specific task.
|
||||
"""
|
||||
compute_effort(t::AbstractDataTask) = 0
|
||||
|
||||
"""
|
||||
compute(t::AbstractDataTask; data...)
|
||||
|
||||
The compute function of a data task, always the identity function, regardless of the specific task.
|
||||
"""
|
||||
compute(t::AbstractDataTask; data...) = data
|
||||
|
||||
"""
|
||||
data(t::AbstractDataTask)
|
||||
|
||||
Return the data of a data task. Given by the task's `.data` field.
|
||||
"""
|
||||
data(t::AbstractDataTask) = getfield(t, :data)
|
||||
|
||||
"""
|
||||
data(t::AbstractComputeTask)
|
||||
|
||||
Return the data of a compute task, always zero, regardless of the specific task.
|
||||
"""
|
||||
data(t::AbstractComputeTask) = 0
|
||||
|
||||
"""
|
||||
compute_effort(t::FusedComputeTask)
|
||||
|
||||
Return the compute effort of a fused compute task.
|
||||
"""
|
||||
function compute_effort(t::FusedComputeTask)
|
||||
(T1, T2) = collect(typeof(t).parameters)
|
||||
return compute_effort(T1()) + compute_effort(T2())
|
||||
end
|
||||
|
||||
# actual compute functions for the tasks can stay undefined for now
|
||||
# compute(t::ComputeTaskU, data::Any) = mycomputation(data)
|
||||
|
||||
"""
|
||||
get_types(::FusedComputeTask{T1, T2})
|
||||
|
||||
Return a tuple of a the fused compute task's components' types.
|
||||
"""
|
||||
get_types(::FusedComputeTask{T1, T2}) where {T1, T2} = (T1, T2)
|
30
src/task/type.jl
Normal file
30
src/task/type.jl
Normal file
@ -0,0 +1,30 @@
|
||||
"""
|
||||
AbstractTask
|
||||
|
||||
The shared base type for any task.
|
||||
"""
|
||||
abstract type AbstractTask end
|
||||
|
||||
"""
|
||||
AbstractComputeTask <: AbstractTask
|
||||
|
||||
The shared base type for any compute task.
|
||||
"""
|
||||
abstract type AbstractComputeTask <: AbstractTask end
|
||||
|
||||
"""
|
||||
AbstractDataTask <: AbstractTask
|
||||
|
||||
The shared base type for any data task.
|
||||
"""
|
||||
abstract type AbstractDataTask <: AbstractTask end
|
||||
|
||||
"""
|
||||
FusedComputeTask{T1 <: AbstractComputeTask, T2 <: AbstractComputeTask} <: AbstractComputeTask
|
||||
|
||||
A fused compute task made up of the computation of first `T1` and then `T2`.
|
||||
|
||||
Also see: [`get_types`](@ref).
|
||||
"""
|
||||
struct FusedComputeTask{T1 <: AbstractComputeTask, T2 <: AbstractComputeTask} <:
|
||||
AbstractComputeTask end
|
@ -1,53 +0,0 @@
|
||||
function compute(t::AbstractTask; data...)
|
||||
error("Need to implement compute()")
|
||||
end
|
||||
|
||||
function compute_effort(t::AbstractTask)
|
||||
# default implementation using compute
|
||||
error("Need to implement compute_effort()")
|
||||
end
|
||||
|
||||
function data(t::AbstractTask)
|
||||
error("Need to implement data()")
|
||||
end
|
||||
|
||||
compute_effort(t::AbstractDataTask) = 0
|
||||
compute(t::AbstractDataTask; data...) = data
|
||||
data(t::AbstractDataTask) = getfield(t, :data)
|
||||
|
||||
data(t::AbstractComputeTask) = 0
|
||||
|
||||
function compute_effort(t::FusedComputeTask)
|
||||
(T1, T2) = collect(typeof(t).parameters)
|
||||
return compute_effort(T1()) + compute_effort(T2())
|
||||
end
|
||||
|
||||
# actual compute functions for the tasks can stay undefined for now
|
||||
# compute(t::ComputeTaskU, data::Any) = mycomputation(data)
|
||||
|
||||
function compute_intensity(t::AbstractTask)::UInt64
|
||||
if data(t) == 0
|
||||
return typemax(UInt64)
|
||||
end
|
||||
return compute_effort(t) / data(t)
|
||||
end
|
||||
|
||||
function show(io::IO, t::FusedComputeTask)
|
||||
(T1, T2) = get_types(t)
|
||||
print(io, "ComputeFuse(", T1(), ", ", T2(), ")")
|
||||
end
|
||||
|
||||
function ==(t1::AbstractTask, t2::AbstractTask)
|
||||
return false
|
||||
end
|
||||
|
||||
function ==(t1::AbstractComputeTask, t2::AbstractComputeTask)
|
||||
return typeof(t1) == typeof(t2)
|
||||
end
|
||||
|
||||
function ==(t1::AbstractDataTask, t2::AbstractDataTask)
|
||||
return data(t1) == data(t2)
|
||||
end
|
||||
|
||||
copy(t::AbstractDataTask) = error("Need to implement copying for your data tasks!")
|
||||
copy(t::AbstractComputeTask) = typeof(t)()
|
@ -1,9 +0,0 @@
|
||||
abstract type AbstractTask end
|
||||
|
||||
abstract type AbstractComputeTask <: AbstractTask end
|
||||
abstract type AbstractDataTask <: AbstractTask end
|
||||
|
||||
struct FusedComputeTask{T1<:AbstractComputeTask, T2<:AbstractComputeTask} <: AbstractComputeTask
|
||||
end
|
||||
|
||||
get_types(::FusedComputeTask{T1, T2}) where {T1, T2} = (T1, T2)
|
61
src/trie.jl
61
src/trie.jl
@ -1,64 +1,101 @@
|
||||
"""
|
||||
NodeIdTrie
|
||||
|
||||
# helper struct for NodeTrie
|
||||
Helper struct for [`NodeTrie`](@ref). After the Trie's first level, every Trie level contains the vector of nodes that had children up to that level, and the TrieNode's children by UUID of the node's children.
|
||||
"""
|
||||
mutable struct NodeIdTrie
|
||||
value::Vector{Node}
|
||||
children::Dict{UUID, NodeIdTrie}
|
||||
end
|
||||
|
||||
# Trie data structure for node reduction, inserts nodes by children
|
||||
# Assumes that given nodes have ordered vectors of children (see sort_node)
|
||||
# First level is the task type and thus does not have a value
|
||||
# Should be constructed with all Types that will be used
|
||||
"""
|
||||
NodeTrie
|
||||
|
||||
Trie data structure for node reduction, inserts nodes by children.
|
||||
Assumes that given nodes have ordered vectors of children (see [`sort_node!`](@ref)).
|
||||
First insertion level is the node's own task type and thus does not have a value (every node has a task type).
|
||||
|
||||
See also: [`insert!`](@ref) and [`collect`](@ref)
|
||||
"""
|
||||
mutable struct NodeTrie
|
||||
children::Dict{DataType, NodeIdTrie}
|
||||
end
|
||||
|
||||
"""
|
||||
NodeTrie()
|
||||
|
||||
Constructor for an empty [`NodeTrie`](@ref).
|
||||
"""
|
||||
function NodeTrie()
|
||||
return NodeTrie(Dict{DataType, NodeIdTrie}())
|
||||
end
|
||||
|
||||
"""
|
||||
NodeIdTrie()
|
||||
|
||||
Constructor for an empty [`NodeIdTrie`](@ref).
|
||||
"""
|
||||
function NodeIdTrie()
|
||||
return NodeIdTrie(Vector{Node}(), Dict{UUID, NodeIdTrie}())
|
||||
end
|
||||
|
||||
"""
|
||||
insert_helper!(trie::NodeIdTrie, node::Node, depth::Int)
|
||||
|
||||
Insert the given node into the trie. The depth is used to iterate through the trie layers, while the function calls itself recursively until it ran through all children of the node.
|
||||
"""
|
||||
function insert_helper!(trie::NodeIdTrie, node::Node, depth::Int)
|
||||
if (length(node.children) == depth)
|
||||
push!(trie.value, node)
|
||||
return nothing
|
||||
end
|
||||
|
||||
|
||||
depth = depth + 1
|
||||
id = node.children[depth].id
|
||||
|
||||
|
||||
if (!haskey(trie.children, id))
|
||||
trie.children[id] = NodeIdTrie()
|
||||
end
|
||||
insert_helper!(trie.children[id], node, depth)
|
||||
return insert_helper!(trie.children[id], node, depth)
|
||||
end
|
||||
|
||||
"""
|
||||
insert!(trie::NodeTrie, node::Node)
|
||||
|
||||
Insert the given node into the trie. It's sorted by its type in the first layer, then by its children in the following layers.
|
||||
"""
|
||||
function insert!(trie::NodeTrie, node::Node)
|
||||
t = typeof(node.task)
|
||||
if (!haskey(trie.children, t))
|
||||
trie.children[t] = NodeIdTrie()
|
||||
end
|
||||
insert_helper!(trie.children[typeof(node.task)], node, 0)
|
||||
return insert_helper!(trie.children[typeof(node.task)], node, 0)
|
||||
end
|
||||
|
||||
"""
|
||||
collect_helper(trie::NodeIdTrie, acc::Set{Vector{Node}})
|
||||
|
||||
Collects the Vectors of this [`NodeIdTrie`](@ref) node and all its children and puts them in the `acc` argument.
|
||||
"""
|
||||
function collect_helper(trie::NodeIdTrie, acc::Set{Vector{Node}})
|
||||
if (length(trie.value) >= 2)
|
||||
push!(acc, trie.value)
|
||||
end
|
||||
|
||||
for (id,child) in trie.children
|
||||
for (id, child) in trie.children
|
||||
collect_helper(child, acc)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
# returns all sets of multiple nodes that have accumulated in leaves
|
||||
"""
|
||||
collect(trie::NodeTrie)
|
||||
|
||||
Return all sets of at least 2 [`Node`](@ref)s that have accumulated in leaves of the trie.
|
||||
"""
|
||||
function collect(trie::NodeTrie)
|
||||
acc = Set{Vector{Node}}()
|
||||
for (t,child) in trie.children
|
||||
for (t, child) in trie.children
|
||||
collect_helper(child, acc)
|
||||
end
|
||||
return acc
|
||||
|
@ -1,18 +1,89 @@
|
||||
function bytes_to_human_readable(bytes::Int64)
|
||||
"""
|
||||
bytes_to_human_readable(bytes)
|
||||
|
||||
Return a human readable string representation of the given number.
|
||||
|
||||
```jldoctest
|
||||
julia> using MetagraphOptimization
|
||||
|
||||
julia> bytes_to_human_readable(4096)
|
||||
"4.0 KiB"
|
||||
```
|
||||
"""
|
||||
function bytes_to_human_readable(bytes)
|
||||
units = ["B", "KiB", "MiB", "GiB", "TiB"]
|
||||
unit_index = 1
|
||||
while bytes >= 1024 && unit_index < length(units)
|
||||
bytes /= 1024
|
||||
unit_index += 1
|
||||
end
|
||||
return string(round(bytes, sigdigits=4), " ", units[unit_index])
|
||||
return string(round(bytes, sigdigits = 4), " ", units[unit_index])
|
||||
end
|
||||
|
||||
"""
|
||||
lt_nodes(n1::Node, n2::Node)
|
||||
|
||||
Less-Than comparison between nodes. Uses the nodes' ids to sort.
|
||||
"""
|
||||
function lt_nodes(n1::Node, n2::Node)
|
||||
return n1.id < n2.id
|
||||
end
|
||||
|
||||
"""
|
||||
sort_node!(node::Node)
|
||||
|
||||
Sort the nodes' parents and children vectors. The vectors are mostly very short so sorting does not take a lot of time.
|
||||
Sorted nodes are required to make the finding of [`NodeReduction`](@ref)s a lot faster using the [`NodeTrie`](@ref) data structure.
|
||||
"""
|
||||
function sort_node!(node::Node)
|
||||
sort!(node.children, lt=lt_nodes)
|
||||
sort!(node.parents, lt=lt_nodes)
|
||||
sort!(node.children, lt = lt_nodes)
|
||||
return sort!(node.parents, lt = lt_nodes)
|
||||
end
|
||||
|
||||
"""
|
||||
mem(graph::DAG)
|
||||
|
||||
Return the memory footprint of the graph in Byte. Should be the same result as `Base.summarysize(graph)` but a lot faster.
|
||||
"""
|
||||
function mem(graph::DAG)
|
||||
size = 0
|
||||
size += Base.summarysize(graph.nodes, exclude = Union{Node})
|
||||
for n in graph.nodes
|
||||
size += mem(n)
|
||||
end
|
||||
|
||||
size += sizeof(graph.appliedOperations)
|
||||
size += sizeof(graph.operationsToApply)
|
||||
|
||||
size += sizeof(graph.possibleOperations)
|
||||
for op in graph.possibleOperations.nodeFusions
|
||||
size += mem(op)
|
||||
end
|
||||
for op in graph.possibleOperations.nodeReductions
|
||||
size += mem(op)
|
||||
end
|
||||
for op in graph.possibleOperations.nodeSplits
|
||||
size += mem(op)
|
||||
end
|
||||
|
||||
size += Base.summarysize(graph.dirtyNodes, exclude = Union{Node})
|
||||
return size += sizeof(diff)
|
||||
end
|
||||
|
||||
"""
|
||||
mem(op::Operation)
|
||||
|
||||
Return the memory footprint of the operation in Byte. Used in [`mem(graph::DAG)`](@ref). Unlike `Base.summarysize()` this doesn't follow all references which would yield (almost) the size of the entire graph.
|
||||
"""
|
||||
function mem(op::Operation)
|
||||
return Base.summarysize(op, exclude = Union{Node})
|
||||
end
|
||||
|
||||
"""
|
||||
mem(op::Operation)
|
||||
|
||||
Return the memory footprint of the node in Byte. Used in [`mem(graph::DAG)`](@ref). Unlike `Base.summarysize()` this doesn't follow all references which would yield (almost) the size of the entire graph.
|
||||
"""
|
||||
function mem(node::Node)
|
||||
return Base.summarysize(node, exclude = Union{Node, Operation})
|
||||
end
|
||||
|
@ -1,85 +1,89 @@
|
||||
using Random
|
||||
|
||||
function test_known_graph(name::String, n, fusion_test=true)
|
||||
@testset "Test $name Graph ($n)" begin
|
||||
graph = parse_abc(joinpath(@__DIR__, "..", "input", "$name.txt"))
|
||||
props = graph_properties(graph)
|
||||
function test_known_graph(name::String, n, fusion_test = true)
|
||||
@testset "Test $name Graph ($n)" begin
|
||||
graph = parse_abc(joinpath(@__DIR__, "..", "input", "$name.txt"))
|
||||
props = graph_properties(graph)
|
||||
|
||||
if (fusion_test)
|
||||
test_node_fusion(graph)
|
||||
if (fusion_test)
|
||||
test_node_fusion(graph)
|
||||
end
|
||||
test_random_walk(graph, n)
|
||||
end
|
||||
test_random_walk(graph, n)
|
||||
end
|
||||
end
|
||||
|
||||
function test_node_fusion(g::DAG)
|
||||
@testset "Test Node Fusion" begin
|
||||
props = graph_properties(g)
|
||||
|
||||
options = get_operations(g)
|
||||
|
||||
nodes_number = length(g.nodes)
|
||||
data = props.data
|
||||
compute_effort = props.compute_effort
|
||||
|
||||
while !isempty(options.nodeFusions)
|
||||
fusion = first(options.nodeFusions)
|
||||
|
||||
@test typeof(fusion) <: NodeFusion
|
||||
|
||||
push_operation!(g, fusion)
|
||||
|
||||
@testset "Test Node Fusion" begin
|
||||
props = graph_properties(g)
|
||||
@test props.data < data
|
||||
@test props.compute_effort == compute_effort
|
||||
|
||||
options = get_operations(g)
|
||||
|
||||
nodes_number = length(g.nodes)
|
||||
data = props.data
|
||||
compute_effort = props.compute_effort
|
||||
|
||||
options = get_operations(g)
|
||||
while !isempty(options.nodeFusions)
|
||||
fusion = first(options.nodeFusions)
|
||||
|
||||
@test typeof(fusion) <: NodeFusion
|
||||
|
||||
push_operation!(g, fusion)
|
||||
|
||||
props = graph_properties(g)
|
||||
@test props.data < data
|
||||
@test props.compute_effort == compute_effort
|
||||
|
||||
nodes_number = length(g.nodes)
|
||||
data = props.data
|
||||
compute_effort = props.compute_effort
|
||||
|
||||
options = get_operations(g)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function test_random_walk(g::DAG, n::Int64)
|
||||
@testset "Test Random Walk ($n)" begin
|
||||
# the purpose here is to do "random" operations and reverse them again and validate that the graph stays the same and doesn't diverge
|
||||
reset_graph!(g)
|
||||
@testset "Test Random Walk ($n)" begin
|
||||
# the purpose here is to do "random" operations and reverse them again and validate that the graph stays the same and doesn't diverge
|
||||
reset_graph!(g)
|
||||
|
||||
properties = graph_properties(g)
|
||||
@test is_valid(g)
|
||||
|
||||
for i = 1:n
|
||||
# choose push or pop
|
||||
if rand(Bool)
|
||||
# push
|
||||
opt = get_operations(g)
|
||||
properties = graph_properties(g)
|
||||
|
||||
# choose one of fuse/split/reduce
|
||||
option = rand(1:3)
|
||||
if option == 1 && !isempty(opt.nodeFusions)
|
||||
push_operation!(g, rand(collect(opt.nodeFusions)))
|
||||
elseif option == 2 && !isempty(opt.nodeReductions)
|
||||
push_operation!(g, rand(collect(opt.nodeReductions)))
|
||||
elseif option == 3 && !isempty(opt.nodeSplits)
|
||||
push_operation!(g, rand(collect(opt.nodeSplits)))
|
||||
for i in 1:n
|
||||
# choose push or pop
|
||||
if rand(Bool)
|
||||
# push
|
||||
opt = get_operations(g)
|
||||
|
||||
# choose one of fuse/split/reduce
|
||||
option = rand(1:3)
|
||||
if option == 1 && !isempty(opt.nodeFusions)
|
||||
push_operation!(g, rand(collect(opt.nodeFusions)))
|
||||
elseif option == 2 && !isempty(opt.nodeReductions)
|
||||
push_operation!(g, rand(collect(opt.nodeReductions)))
|
||||
elseif option == 3 && !isempty(opt.nodeSplits)
|
||||
push_operation!(g, rand(collect(opt.nodeSplits)))
|
||||
else
|
||||
i = i - 1
|
||||
end
|
||||
else
|
||||
i = i - 1
|
||||
end
|
||||
else
|
||||
# pop
|
||||
if (can_pop(g))
|
||||
pop_operation!(g)
|
||||
else
|
||||
i = i - 1
|
||||
# pop
|
||||
if (can_pop(g))
|
||||
pop_operation!(g)
|
||||
else
|
||||
i = i - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
reset_graph!(g)
|
||||
|
||||
@test is_valid(g)
|
||||
|
||||
@test properties == graph_properties(g)
|
||||
end
|
||||
|
||||
reset_graph!(g)
|
||||
|
||||
@test properties == graph_properties(g)
|
||||
end
|
||||
end
|
||||
|
||||
Random.seed!(0)
|
||||
|
@ -51,6 +51,8 @@ import MetagraphOptimization.make_node
|
||||
insert_edge!(graph, BD, B1C_2, false)
|
||||
insert_edge!(graph, CD, C1C, false)
|
||||
|
||||
@test is_valid(graph)
|
||||
|
||||
@test is_exit_node(d_exit)
|
||||
@test is_entry_node(AD)
|
||||
@test is_entry_node(BD)
|
||||
@ -75,6 +77,8 @@ import MetagraphOptimization.make_node
|
||||
push_operation!(graph, nr)
|
||||
opt = get_operations(graph)
|
||||
|
||||
@test is_valid(graph)
|
||||
|
||||
@test length(opt) == (nodeFusions = 4, nodeReductions = 0, nodeSplits = 1)
|
||||
#println("After 2 Node Reductions:\n", opt)
|
||||
|
||||
@ -89,5 +93,7 @@ import MetagraphOptimization.make_node
|
||||
opt = get_operations(graph)
|
||||
@test length(opt) == (nodeFusions = 6, nodeReductions = 1, nodeSplits = 1)
|
||||
#println("After reverting to the initial state:\n", opt)
|
||||
|
||||
@test is_valid(graph)
|
||||
end
|
||||
println("Node Reduction Unit Tests Complete!")
|
||||
|
@ -11,8 +11,10 @@ import MetagraphOptimization.partners
|
||||
@test length(graph.appliedOperations) == 0
|
||||
@test length(graph.operationsToApply) == 0
|
||||
@test length(graph.dirtyNodes) == 0
|
||||
@test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
|
||||
@test length(get_operations(graph)) == (nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
|
||||
@test length(graph.diff) ==
|
||||
(addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
|
||||
@test length(get_operations(graph)) ==
|
||||
(nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
|
||||
|
||||
# s to output (exit node)
|
||||
d_exit = insert_node!(graph, make_node(DataTask(10)), false)
|
||||
@ -105,7 +107,10 @@ import MetagraphOptimization.partners
|
||||
@test length(graph.appliedOperations) == 0
|
||||
@test length(graph.operationsToApply) == 0
|
||||
@test length(graph.dirtyNodes) == 26
|
||||
@test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
|
||||
@test length(graph.diff) ==
|
||||
(addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
|
||||
|
||||
@test is_valid(graph)
|
||||
|
||||
@test is_entry_node(d_PB)
|
||||
@test is_entry_node(d_PA)
|
||||
@ -130,7 +135,8 @@ import MetagraphOptimization.partners
|
||||
@test length(siblings(s0)) == 1
|
||||
|
||||
operations = get_operations(graph)
|
||||
@test length(operations) == (nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
|
||||
@test length(operations) ==
|
||||
(nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
|
||||
@test length(graph.dirtyNodes) == 0
|
||||
|
||||
@test operations == get_operations(graph)
|
||||
@ -139,7 +145,7 @@ import MetagraphOptimization.partners
|
||||
properties = graph_properties(graph)
|
||||
@test properties.compute_effort == 134
|
||||
@test properties.data == 62
|
||||
@test properties.compute_intensity ≈ 134/62
|
||||
@test properties.compute_intensity ≈ 134 / 62
|
||||
@test properties.nodes == 26
|
||||
@test properties.edges == 25
|
||||
|
||||
@ -151,7 +157,8 @@ import MetagraphOptimization.partners
|
||||
@test length(graph.operationsToApply) == 1
|
||||
@test first(graph.operationsToApply) == nf
|
||||
@test length(graph.dirtyNodes) == 0
|
||||
@test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
|
||||
@test length(graph.diff) ==
|
||||
(addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
|
||||
|
||||
# this applies pending operations
|
||||
properties = graph_properties(graph)
|
||||
@ -164,12 +171,13 @@ import MetagraphOptimization.partners
|
||||
@test properties.edges == 23
|
||||
@test properties.compute_effort == 134
|
||||
@test properties.data < 62
|
||||
@test properties.compute_intensity > 134/62
|
||||
@test properties.compute_intensity > 134 / 62
|
||||
|
||||
operations = get_operations(graph)
|
||||
@test length(graph.dirtyNodes) == 0
|
||||
|
||||
@test length(operations) == (nodeFusions = 9, nodeReductions = 0, nodeSplits = 0)
|
||||
@test length(operations) ==
|
||||
(nodeFusions = 9, nodeReductions = 0, nodeSplits = 0)
|
||||
@test !isempty(operations)
|
||||
|
||||
possibleNF = 9
|
||||
@ -177,12 +185,14 @@ import MetagraphOptimization.partners
|
||||
push_operation!(graph, first(operations.nodeFusions))
|
||||
operations = get_operations(graph)
|
||||
possibleNF = possibleNF - 1
|
||||
@test length(operations) == (nodeFusions = possibleNF, nodeReductions = 0, nodeSplits = 0)
|
||||
@test length(operations) ==
|
||||
(nodeFusions = possibleNF, nodeReductions = 0, nodeSplits = 0)
|
||||
end
|
||||
|
||||
@test isempty(operations)
|
||||
|
||||
@test length(operations) == (nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
|
||||
@test length(operations) ==
|
||||
(nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
|
||||
@test length(graph.dirtyNodes) == 0
|
||||
@test length(graph.nodes) == 6
|
||||
@test length(graph.appliedOperations) == 10
|
||||
@ -200,9 +210,12 @@ import MetagraphOptimization.partners
|
||||
@test properties.edges == 25
|
||||
@test properties.compute_effort == 134
|
||||
@test properties.data == 62
|
||||
@test properties.compute_intensity ≈ 134/62
|
||||
@test properties.compute_intensity ≈ 134 / 62
|
||||
|
||||
operations = get_operations(graph)
|
||||
@test length(operations) == (nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
|
||||
@test length(operations) ==
|
||||
(nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
|
||||
|
||||
@test is_valid(graph)
|
||||
end
|
||||
println("Graph Unit Tests Complete!")
|
||||
|
@ -3,7 +3,8 @@
|
||||
nC1 = MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskU())
|
||||
nC2 = MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskV())
|
||||
nC3 = MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskP())
|
||||
nC4 = MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskSum())
|
||||
nC4 =
|
||||
MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskSum())
|
||||
|
||||
nD1 = MetagraphOptimization.make_node(MetagraphOptimization.DataTask(10))
|
||||
nD2 = MetagraphOptimization.make_node(MetagraphOptimization.DataTask(20))
|
||||
|
@ -28,15 +28,6 @@
|
||||
@test MetagraphOptimization.data(Data10) == 10
|
||||
@test MetagraphOptimization.data(Data20) == 20
|
||||
|
||||
@test MetagraphOptimization.compute_intensity(S1) == typemax(UInt64)
|
||||
@test MetagraphOptimization.compute_intensity(S2) == typemax(UInt64)
|
||||
@test MetagraphOptimization.compute_intensity(U) == typemax(UInt64)
|
||||
@test MetagraphOptimization.compute_intensity(V) == typemax(UInt64)
|
||||
@test MetagraphOptimization.compute_intensity(P) == typemax(UInt64)
|
||||
@test MetagraphOptimization.compute_intensity(Sum) == typemax(UInt64)
|
||||
@test MetagraphOptimization.compute_intensity(Data10) == 0
|
||||
@test MetagraphOptimization.compute_intensity(Data20) == 0
|
||||
|
||||
@test S1 != S2
|
||||
@test Data10 != Data20
|
||||
|
||||
|
@ -5,7 +5,9 @@
|
||||
@test MetagraphOptimization.bytes_to_human_readable(1025) == "1.001 KiB"
|
||||
@test MetagraphOptimization.bytes_to_human_readable(684235) == "668.2 KiB"
|
||||
@test MetagraphOptimization.bytes_to_human_readable(86214576) == "82.22 MiB"
|
||||
@test MetagraphOptimization.bytes_to_human_readable(9241457698) == "8.607 GiB"
|
||||
@test MetagraphOptimization.bytes_to_human_readable(3218598654367) == "2.927 TiB"
|
||||
@test MetagraphOptimization.bytes_to_human_readable(9241457698) ==
|
||||
"8.607 GiB"
|
||||
@test MetagraphOptimization.bytes_to_human_readable(3218598654367) ==
|
||||
"2.927 TiB"
|
||||
end
|
||||
println("Utility Unit Tests Complete!")
|
||||
|
Reference in New Issue
Block a user