Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Attestation aggregation: optimizations and benchmarks #7938

Merged
merged 95 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
13d4657
profitablity tests
farazdagi Nov 23, 2020
5cfe6c2
cleanup benchmark
farazdagi Nov 23, 2020
644d1ae
Merge branch 'master' into maxcover-optimizations-and-benchmarks
farazdagi Nov 23, 2020
31f6000
fix deduplication function
farazdagi Nov 23, 2020
9159475
dedup: move method to atts list
farazdagi Nov 24, 2020
0d473a1
proper substring handling
farazdagi Nov 24, 2020
3c563e9
refactor validate method
farazdagi Nov 25, 2020
798d408
Merge branch 'master' into maxcover-optimizations-and-benchmarks
farazdagi Nov 25, 2020
670127e
update benchmarks
farazdagi Nov 25, 2020
f576205
prepare proposer test
farazdagi Nov 27, 2020
5717cfb
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Dec 4, 2020
ccfcb55
remove redundant code
farazdagi Dec 4, 2020
9592a13
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Dec 7, 2020
6bdf64b
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Dec 11, 2020
50af7df
reset test
farazdagi Dec 11, 2020
c0b6dc5
remove dedup from maxcover - moved to proposer
farazdagi Dec 11, 2020
13fb842
remove redundant test
farazdagi Dec 11, 2020
66b1728
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Dec 13, 2020
7947359
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Dec 16, 2020
1d20050
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Dec 30, 2020
c844121
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 5, 2021
62ed4ac
remove lower level check for bit length
farazdagi Jan 6, 2021
1d48d0e
optimize candidate validation on att aggregation
farazdagi Jan 6, 2021
7cd0ece
restore test
farazdagi Jan 6, 2021
6d36c3f
fix test
farazdagi Jan 6, 2021
f0c99f1
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 6, 2021
923ab23
Merge branch 'attagg-optimize-candidate-validation' into maxcover-opt…
farazdagi Jan 6, 2021
0218e3b
fix test
farazdagi Jan 6, 2021
2003899
remove dedup functionality
farazdagi Jan 6, 2021
c0c17d5
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 6, 2021
2b1f7a1
add benchmark
farazdagi Jan 6, 2021
d726565
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 6, 2021
3fedb51
optimize list usage
farazdagi Jan 7, 2021
7fea198
Attestation aggregration: remove redundant dedup routine
farazdagi Jan 7, 2021
dba11db
fix func call
farazdagi Jan 7, 2021
384714d
Merge branch 'attagg-remove-redundant-deduplication' into maxcover-op…
farazdagi Jan 7, 2021
ae2e675
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 9, 2021
a682184
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 9, 2021
04fccaa
experiment with bitset based cover
farazdagi Jan 15, 2021
5229f58
add benchmark
farazdagi Jan 15, 2021
aa9d555
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 15, 2021
9a005f9
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 20, 2021
5edf450
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 20, 2021
1c68118
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 20, 2021
6a845f6
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 21, 2021
edae066
samplem implementation using Bilist64
farazdagi Jan 21, 2021
4df2234
add tests
farazdagi Jan 21, 2021
a0a0c41
remove redundant code
farazdagi Jan 21, 2021
4ba97fa
remove tmp comments
farazdagi Jan 21, 2021
0afbd92
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 21, 2021
4eb74aa
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 22, 2021
130688c
unskip test
farazdagi Jan 22, 2021
b21f424
update benchmarks
farazdagi Jan 22, 2021
018d7b8
gazelle
farazdagi Jan 22, 2021
63aec31
process err
farazdagi Jan 22, 2021
4db99ec
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 26, 2021
d7f637b
optimized max-cover
farazdagi Jan 27, 2021
b580283
Max-cover: optimized implementation based on Bitlist64
farazdagi Jan 28, 2021
e647ad0
gazelle
farazdagi Jan 28, 2021
d739782
re-arrange overlaps check
farazdagi Jan 28, 2021
a3f0613
minor comments
farazdagi Jan 28, 2021
72843fa
add Bitlists64WithMultipleBitSet
farazdagi Jan 28, 2021
be9e411
update benchmarks
farazdagi Jan 28, 2021
0e90491
gazelle
farazdagi Jan 28, 2021
f8ff9cd
Merge branch 'maxcover-optimizations-bitlist64' into maxcover-optimiz…
farazdagi Jan 28, 2021
cf70cd5
add TestAggregateAttestations_rearrangeProcessedAttestations
farazdagi Jan 28, 2021
e7838ed
minor updates to rearrange method
farazdagi Jan 28, 2021
318aa13
add link to design doc
farazdagi Jan 28, 2021
e862c06
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 28, 2021
f326b30
remove redundant methods
farazdagi Jan 28, 2021
9cbec4d
simplify test
farazdagi Jan 28, 2021
a39165a
add TestAggregateAttestations_aggregateAttestations
farazdagi Jan 28, 2021
310f56b
fix issues
farazdagi Jan 29, 2021
840401a
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 29, 2021
6d9ebc3
fix assignment
farazdagi Jan 29, 2021
72c8c36
use ToBitlist(), ToBitlist64()
farazdagi Jan 29, 2021
9dac462
fixes test
farazdagi Jan 29, 2021
eafe416
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 29, 2021
deeb43e
benchmarks
farazdagi Jan 29, 2021
de24d7f
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Jan 29, 2021
07c1dce
fix typo
farazdagi Jan 29, 2021
c427af3
allow opt_max_cover opt-int flag
farazdagi Jan 29, 2021
9ba9e0f
update benchmarks
farazdagi Jan 29, 2021
3c82e3a
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Feb 1, 2021
146ae9a
reset e2e
farazdagi Feb 1, 2021
a721860
Merge branch 'maxcover-optimizations-and-benchmarks' of github.com:pr…
farazdagi Feb 1, 2021
a05dcdf
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Feb 2, 2021
c27f029
fix test
farazdagi Feb 2, 2021
028b806
enable opt_max_cover in e2e tests
farazdagi Feb 2, 2021
c374bb2
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
rauljordan Feb 3, 2021
b647606
Merge branch 'develop' into maxcover-optimizations-and-benchmarks
farazdagi Feb 3, 2021
5b3f9b9
Merge refs/heads/develop into maxcover-optimizations-and-benchmarks
prylabs-bulldozer[bot] Feb 3, 2021
4d83fca
Merge refs/heads/develop into maxcover-optimizations-and-benchmarks
prylabs-bulldozer[bot] Feb 3, 2021
7d95118
Merge refs/heads/develop into maxcover-optimizations-and-benchmarks
prylabs-bulldozer[bot] Feb 3, 2021
d376ef1
Merge refs/heads/develop into maxcover-optimizations-and-benchmarks
prylabs-bulldozer[bot] Feb 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions shared/aggregation/attestations/attestations.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const (

// MaxCoverAggregation is a strategy based on Maximum Coverage greedy algorithm.
MaxCoverAggregation AttestationAggregationStrategy = "max_cover"

// OptMaxCoverAggregation is a strategy based on Maximum Coverage greedy algorithm.
// This new variant is optimized and relies on Bitlist64 (once fully tested, `max_cover`
// strategy will be replaced with this one).
OptMaxCoverAggregation AttestationAggregationStrategy = "opt_max_cover"
)

// AttestationAggregationStrategy defines attestation aggregation strategy.
Expand All @@ -37,13 +42,23 @@ var _ = logrus.WithField("prefix", "aggregation.attestations")
var ErrInvalidAttestationCount = errors.New("invalid number of attestations")

// Aggregate aggregates attestations. The minimal number of attestations is returned.
// Aggregation occurs in-place i.e. contents of input array will be modified. Should you need to
// preserve input attestations, clone them before aggregating:
//
// clonedAtts := make([]*ethpb.Attestation, len(atts))
// for i, a := range atts {
// clonedAtts[i] = stateTrie.CopyAttestation(a)
// }
// aggregatedAtts, err := attaggregation.Aggregate(clonedAtts)
func Aggregate(atts []*ethpb.Attestation) ([]*ethpb.Attestation, error) {
strategy := AttestationAggregationStrategy(featureconfig.Get().AttestationAggregationStrategy)
switch strategy {
case "", NaiveAggregation:
return NaiveAttestationAggregation(atts)
case MaxCoverAggregation:
return MaxCoverAttestationAggregation(atts)
case OptMaxCoverAggregation:
return optMaxCoverAttestationAggregation(atts)
default:
return nil, errors.Wrapf(aggregation.ErrInvalidStrategy, "%q", strategy)
}
Expand Down
104 changes: 50 additions & 54 deletions shared/aggregation/attestations/attestations_bench_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package attestations

import (
"fmt"
"testing"

ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-bitfield"
stateTrie "github.com/prysmaticlabs/prysm/beacon-chain/state"
aggtesting "github.com/prysmaticlabs/prysm/shared/aggregation/testing"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/bls/common"
Expand Down Expand Up @@ -33,75 +36,68 @@ func BenchmarkAggregateAttestations_Aggregate(b *testing.B) {
inputs []bitfield.Bitlist
}{
{
name: "64 attestations with single bit set",
inputs: aggtesting.BitlistsWithSingleBitSet(64, bitlistLen),
},
{
name: "64 attestations with 8 random bits set",
inputs: aggtesting.BitlistsWithMultipleBitSet(b, 64, bitlistLen, 8),
},
{
name: "64 attestations with 16 random bits set",
inputs: aggtesting.BitlistsWithMultipleBitSet(b, 64, bitlistLen, 16),
},
{
name: "64 attestations with 32 random bits set",
inputs: aggtesting.BitlistsWithMultipleBitSet(b, 64, bitlistLen, 32),
},
{
name: "256 attestations with 32 random bits set",
inputs: aggtesting.BitlistsWithMultipleBitSet(b, 256, bitlistLen, 32),
name: "256 attestations with single bit set",
inputs: aggtesting.BitlistsWithSingleBitSet(256, bitlistLen),
},
{
name: "256 attestations with 64 random bits set",
inputs: aggtesting.BitlistsWithMultipleBitSet(b, 256, bitlistLen, 64),
},
{
name: "128 attestations with single bit set",
inputs: aggtesting.BitlistsWithSingleBitSet(128, bitlistLen),
},
{
name: "256 attestations with single bit set",
inputs: aggtesting.BitlistsWithSingleBitSet(256, bitlistLen),
},
{
name: "512 attestations with single bit set",
inputs: aggtesting.BitlistsWithSingleBitSet(512, bitlistLen),
},
{
name: "1024 attestations with single bit set",
inputs: aggtesting.BitlistsWithSingleBitSet(1024, bitlistLen),
name: "1024 attestations with 64 random bits set",
inputs: aggtesting.BitlistsWithMultipleBitSet(b, 1024, bitlistLen, 64),
},
}

b.Run("max-cover", func(b *testing.B) {
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
atts := aggtesting.MakeAttestationsFromBitlists(tt.inputs)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Aggregate(atts)
require.NoError(b, err)
}
})
runner := func(atts []*ethpb.Attestation) {
attsCopy := make([]*ethpb.Attestation, len(atts))
for i, att := range atts {
attsCopy[i] = stateTrie.CopyAttestation(att)
}
})
_, err := Aggregate(attsCopy)
require.NoError(b, err)
}

b.Run("naive", func(b *testing.B) {
resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{
AttestationAggregationStrategy: string(NaiveAggregation),
for _, tt := range tests {
b.Run(fmt.Sprintf("naive_%s", tt.name), func(b *testing.B) {
b.StopTimer()
resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{
AttestationAggregationStrategy: string(NaiveAggregation),
})
atts := aggtesting.MakeAttestationsFromBitlists(tt.inputs)
defer resetCfg()
b.StartTimer()
for i := 0; i < b.N; i++ {
runner(atts)
}
})
defer resetCfg()

for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
atts := aggtesting.MakeAttestationsFromBitlists(tt.inputs)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Aggregate(atts)
require.NoError(b, err)
}
b.Run(fmt.Sprintf("max-cover_%s", tt.name), func(b *testing.B) {
b.StopTimer()
resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{
AttestationAggregationStrategy: string(MaxCoverAggregation),
})
}
})
atts := aggtesting.MakeAttestationsFromBitlists(tt.inputs)
defer resetCfg()
b.StartTimer()
for i := 0; i < b.N; i++ {
runner(atts)
}
})
b.Run(fmt.Sprintf("opt-max-cover_%s", tt.name), func(b *testing.B) {
b.StopTimer()
resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{
AttestationAggregationStrategy: string(OptMaxCoverAggregation),
})
atts := aggtesting.MakeAttestationsFromBitlists(tt.inputs)
defer resetCfg()
b.StartTimer()
for i := 0; i < b.N; i++ {
runner(atts)
}
})
}
}
9 changes: 8 additions & 1 deletion shared/aggregation/attestations/attestations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestMain(m *testing.M) {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetOutput(ioutil.Discard)
resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{
AttestationAggregationStrategy: string(MaxCoverAggregation),
AttestationAggregationStrategy: string(OptMaxCoverAggregation),
})
defer resetCfg()
m.Run()
Expand Down Expand Up @@ -240,6 +240,13 @@ func TestAggregateAttestations_Aggregate(t *testing.T) {
defer resetCfg()
runner()
})
t.Run(fmt.Sprintf("%s/%s", tt.name, OptMaxCoverAggregation), func(t *testing.T) {
resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{
AttestationAggregationStrategy: string(OptMaxCoverAggregation),
})
defer resetCfg()
runner()
})
}
}

Expand Down
159 changes: 159 additions & 0 deletions shared/aggregation/attestations/maxcover.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,96 @@ func MaxCoverAttestationAggregation(atts []*ethpb.Attestation) ([]*ethpb.Attesta
return aggregated.merge(unaggregated.filterContained()), nil
}

// optMaxCoverAttestationAggregation relies on Maximum Coverage greedy algorithm for aggregation.
// Aggregation occurs in many rounds, up until no more aggregation is possible (all attestations
// are overlapping).
// NB: this method will replace the MaxCoverAttestationAggregation() above (and will be renamed to it).
// See https://hackmd.io/@farazdagi/in-place-attagg for design and rationale.
func optMaxCoverAttestationAggregation(atts []*ethpb.Attestation) ([]*ethpb.Attestation, error) {
if len(atts) < 2 {
return atts, nil
}

if err := attList(atts).validate(); err != nil {
if errors.Is(err, aggregation.ErrBitsDifferentLen) {
return atts, nil
}
return nil, err
}

// In the future this conversion will be redundant, as attestation bitlist will be of a Bitlist64
// type, so incoming `atts` parameters can be used as candidates list directly.
candidates := make([]*bitfield.Bitlist64, len(atts))
for i := 0; i < len(atts); i++ {
candidates[i] = atts[i].AggregationBits.ToBitlist64()
}
coveredBitsSoFar := bitfield.NewBitlist64(candidates[0].Len())

// In order not to re-allocate anything we rely on the very same underlying array, which
// can only shrink (while the `aggregated` slice length can increase).
// The `aggregated` slice grows by combining individual attestations and appending to that slice.
// Both aggregated and non-aggregated slices operate on the very same underlying array.
aggregated := atts[:0]
unaggregated := atts

// Aggregation over n/2 rounds is enough to find all aggregatable items (exits earlier if there
// are many items that can be aggregated).
for i := 0; i < len(atts)/2; i++ {
if len(unaggregated) < 2 {
break
}

// Find maximum non-overlapping coverage for subset of still non-processed candidates.
roundCandidates := candidates[len(aggregated) : len(aggregated)+len(unaggregated)]
selectedKeys, coverage, err := aggregation.MaxCover(
roundCandidates, len(roundCandidates), false /* allowOverlaps */)
if err != nil {
// Return aggregated attestations, and attestations that couldn't be aggregated.
return append(aggregated, unaggregated...), err
}

// Exit earlier, if possible cover does not allow aggregation (less than two items).
if selectedKeys.Count() < 2 {
break
}

// Pad selected key indexes, as `roundCandidates` is a subset of `candidates`.
keys := padSelectedKeys(selectedKeys.BitIndices(), len(aggregated))

// Create aggregated attestation and update solution lists. Process aggregates only if they
// feature at least one unknown bit i.e. can increase the overall coverage.
if coveredBitsSoFar.XorCount(coverage) > 0 {
aggIdx, err := aggregateAttestations(atts, keys, coverage)
if err != nil {
return append(aggregated, unaggregated...), err
}

// Unless we are already at the right position, swap aggregation and the first non-aggregated item.
idx0 := len(aggregated)
if idx0 < aggIdx {
atts[idx0], atts[aggIdx] = atts[aggIdx], atts[idx0]
candidates[idx0], candidates[aggIdx] = candidates[aggIdx], candidates[idx0]
}

// Expand to the newly created aggregate.
aggregated = atts[:idx0+1]

// Shift the starting point of the slice to the right.
unaggregated = unaggregated[1:]

// Update covered bits map.
coveredBitsSoFar.NoAllocOr(coverage, coveredBitsSoFar)
keys = keys[1:]
}

// Remove processed attestations.
rearrangeProcessedAttestations(atts, candidates, keys)
unaggregated = unaggregated[:len(unaggregated)-len(keys)]
}

return append(aggregated, attList(unaggregated).filterContained()...), nil
}

// NewMaxCover returns initialized Maximum Coverage problem for attestations aggregation.
func NewMaxCover(atts []*ethpb.Attestation) *aggregation.MaxCoverProblem {
candidates := make([]*aggregation.MaxCoverCandidate, len(atts))
Expand Down Expand Up @@ -91,6 +181,75 @@ func (al attList) aggregate(coverage bitfield.Bitlist) (*ethpb.Attestation, erro
}, nil
}

// padSelectedKeys adds additional value to every key.
func padSelectedKeys(keys []int, pad int) []int {
for i, key := range keys {
keys[i] = key + pad
}
return keys
}

// aggregateAttestations combines signatures of selected attestations into a single aggregate attestation, and
// pushes that aggregated attestation into the position of the first of selected attestations.
func aggregateAttestations(atts []*ethpb.Attestation, keys []int, coverage *bitfield.Bitlist64) (targetIdx int, err error) {
if len(keys) < 2 || atts == nil || len(atts) < 2 {
return targetIdx, errors.Wrap(ErrInvalidAttestationCount, "cannot aggregate")
}
if coverage == nil || coverage.Count() == 0 {
return targetIdx, errors.New("invalid or empty coverage")
}

var data *ethpb.AttestationData
signs := make([]bls.Signature, 0, len(keys))
for i, idx := range keys {
sig, err := signatureFromBytes(atts[idx].Signature)
if err != nil {
return targetIdx, err
}
signs = append(signs, sig)
if i == 0 {
data = stateTrie.CopyAttestationData(atts[idx].Data)
targetIdx = idx
}
}
// Put aggregated attestation at a position of the first selected attestation.
atts[targetIdx] = &ethpb.Attestation{
// Append size byte, which will be unnecessary on switch to Bitlist64.
AggregationBits: coverage.ToBitlist(),
Data: data,
Signature: aggregateSignatures(signs).Marshal(),
}
return
}

// rearrangeProcessedAttestations pushes processed attestations to the end of the slice, returning
// the number of items re-arranged (so that caller can cut the slice, and allow processed items to be
// garbage collected).
func rearrangeProcessedAttestations(atts []*ethpb.Attestation, candidates []*bitfield.Bitlist64, processedKeys []int) {
if atts == nil || candidates == nil || processedKeys == nil {
return
}
// Set all selected keys to nil.
for _, idx := range processedKeys {
atts[idx] = nil
candidates[idx] = nil
}
// Re-arrange nil items, move them to end of slice.
sort.Ints(processedKeys)
lastIdx := len(atts) - 1
for _, idx0 := range processedKeys {
// Make sure that nil items are swapped for non-nil items only.
for lastIdx > idx0 && atts[lastIdx] == nil {
lastIdx--
}
if idx0 == lastIdx {
break
}
atts[idx0], atts[lastIdx] = atts[lastIdx], atts[idx0]
candidates[idx0], candidates[lastIdx] = candidates[lastIdx], candidates[idx0]
}
}

// merge combines two attestation lists into one.
func (al attList) merge(al1 attList) attList {
return append(al, al1...)
Expand Down
Loading