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

benchmarking infrastructure #61

Merged
merged 5 commits into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
/Packages
/*.xcodeproj
xcuserdata/
/Package.resolved
/.swiftpm
/.vscode
/.idea
25 changes: 25 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"object": {
"pins": [
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "223d62adc52d51669ae2ee19bdb8b7d9fd6fcd9c",
"version": "0.0.6"
}
},
{
"package": "Benchmark",
"repositoryURL": "https://github.com/google/swift-benchmark.git",
"state": {
"branch": "master",
"revision": "a952f1d7deed2368805ac29f09da6a676dde8015",
"version": null
}
}
]
},
"version": 1
}
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/google/swift-benchmark.git", .branch("master")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "SwiftFusion",
dependencies: []),
.target(
name: "Benchmarks",
dependencies: [
"Benchmark",
"SwiftFusion",
]),
.target(
name: "Pose2SLAMG2O",
dependencies: ["SwiftFusion"],
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

Differentiable Swift based sensor fusion library.

## Run tests

```
swift test
```

## Run benchmarks

```
swift run -c release -Xswiftc -cross-module-optimization Benchmarks
```

## Update dependency versions

```
swift package update
```

# LICENSE


Expand Down
70 changes: 70 additions & 0 deletions Sources/Benchmarks/Pose2SLAM.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2020 The SwiftFusion Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Benchmarks Pose2SLAM solutions.

import Benchmark
import SwiftFusion

let pose2SLAM = BenchmarkSuite(name: "Pose2SLAM") { suite in

let intelDataset = try! G2OFactorGraph(fromG2O: try! cachedDataset("input_INTEL_g2o.txt"))
check(intelDataset.graph.error(intelDataset.initialGuess), near: 73565.64, accuracy: 1e-2)

// Uses `NonlinearFactorGraph` on the Intel dataset.
// The solvers are configured to run for a constant number of steps.
// The nonlinear solver is 5 iterations of Gauss-Newton.
// The linear solver is 100 iterations of CGLS.
suite.benchmark(
"NonlinearFactorGraph, Intel, 5 Gauss-Newton steps, 100 CGLS steps",
settings: .iterations(1)

Choose a reason for hiding this comment

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

Results are going to be quite noisy if you only run a single iteration. Is it not practical to run it for longer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is benchmarking a whole solver, and it's pretty slow right now (~12 seconds).

Copy link

@shabalind shabalind May 18, 2020

Choose a reason for hiding this comment

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

I see, thanks for the clarification.

) {
var val = intelDataset.initialGuess
for _ in 0..<5 {
Copy link

@shabalind shabalind May 18, 2020

Choose a reason for hiding this comment

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

Usually, it's better to avoid loops in benchmarks. Instead, you can put just the loop body as a benchmark, and then customize number of iterations with .iterations (or better leave it off to be automatically detected by the framework).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This loop is part of the solver logic that we are benchmarking, so I think it makes sense to have it in the benchmark. Eventually we're going to have an API for the solver and the benchmark body will look like solve(graph, initialGuess) and this loop will be hidden inside the solve function.

Copy link
Collaborator

@ProfFan ProfFan May 18, 2020

Choose a reason for hiding this comment

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

Also in the final solver this should be a while error < tol && i < max_iteration

let gfg = intelDataset.graph.linearize(val)
let optimizer = CGLS(precision: 0, max_iteration: 100)
var dx = VectorValues()
for i in 0..<val.count {
dx.insert(i, Vector(zeros: 3))
}
optimizer.optimize(gfg: gfg, initial: &dx)
val.move(along: dx)
}
check(intelDataset.graph.error(val), near: 35.59, accuracy: 1e-2)
}
}

/// Builds an initial guess and a factor graph from a g2o file.
struct G2OFactorGraph: G2OReader {
/// The initial guess.
var initialGuess: Values = Values()

/// The factor graph representing the measurements.
var graph: NonlinearFactorGraph = NonlinearFactorGraph()

public mutating func addInitialGuess(index: Int, pose: Pose2) {
initialGuess.insert(index, pose)
}

public mutating func addMeasurement(frameIndex: Int, measuredIndex: Int, pose: Pose2) {
graph += BetweenFactor(frameIndex, measuredIndex, pose)
}
}

func check(_ actual: Double, near expected: Double, accuracy: Double) {
if abs(actual - expected) > accuracy {
print("ERROR: \(actual) != \(expected) (accuracy \(accuracy))")
fatalError()
}
}
19 changes: 19 additions & 0 deletions Sources/Benchmarks/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2020 The SwiftFusion Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Benchmark

Benchmark.main([
pose2SLAM
])
70 changes: 70 additions & 0 deletions Sources/SwiftFusion/Datasets/DatasetCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2020 The SwiftFusion Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation
#if !os(macOS)
import FoundationNetworking
#endif

/// Dictionary from dataset id to dataset url.
fileprivate let datasets = [
"input_INTEL_g2o.txt":
"https://github.com/pkchen1129/Mobile-Robotics/raw/8551df10ba8af36801403daeba710c1f9c9e54cd/ps4/code/dataset/input_INTEL_g2o.txt"
]

/// Returns a dataset cached on the local system.
///
/// If the dataset with `id` is available on the local system, returns it immediately.
/// Otherwise, downloads the dataset to the cache and then returns it.
///
/// - Parameter `cacheDirectory`: The directory where the cached datasets are stored.
public func cachedDataset(
_ id: String,
cacheDirectory: URL = URL(fileURLWithPath: NSHomeDirectory())
.appendingPathComponent(".SwiftFusionDatasetCache", isDirectory: true)
) throws -> URL {
try createDirectoryIfMissing(at: cacheDirectory.path)
let cacheEntry = cacheDirectory.appendingPathComponent(id)
if FileManager.default.fileExists(atPath: cacheEntry.path) {
return cacheEntry
}
guard let url = datasets[id] else {
throw DatasetCacheError(message: "No such dataset: \(id)")
}
print("Downloading \(url) to \(cacheEntry)")
guard let source = URL(string: url) else {
throw DatasetCacheError(message: "Could not parse URL: \(url)")
}
let data = try Data.init(contentsOf: source)
try data.write(to: cacheEntry)
print("Downloaded \(cacheEntry)!")
return cacheEntry
}

/// An error from getting a cached dataset.
public struct DatasetCacheError: Swift.Error {
public let message: String
}

/// Creates a directory at a path, if missing. If the directory exists, this does nothing.
///
/// - Parameters:
/// - path: The path of the desired directory.
fileprivate func createDirectoryIfMissing(at path: String) throws {
guard !FileManager.default.fileExists(atPath: path) else { return }
try FileManager.default.createDirectory(
atPath: path,
withIntermediateDirectories: true,
attributes: nil)
}
11 changes: 10 additions & 1 deletion Sources/SwiftFusion/Datasets/G2OReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import Foundation
///
/// See https://lucacarlone.mit.edu/datasets/ for g2o specification and example datasets.
public protocol G2OReader {
/// Creates an empty reader.
init()

/// Adds an initial guess that vertex `index` has pose `pose`.
mutating func addInitialGuess(index: Int, pose: Pose2)

Expand All @@ -29,7 +32,13 @@ public protocol G2OReader {
}

extension G2OReader {
/// Reads a g2o dataset from `url`.
/// Creates a g2o dataset from `url`.
public init(fromG2O url: URL) throws {
self.init()
try self.read(fromG2O: url)
}

/// Adds the g2o dataset at `url` into `self`.
public mutating func read(fromG2O url: URL) throws {
let lines = try String(contentsOf: url).split(separator: "\n")
for (lineIndex, line) in lines.enumerated() {
Expand Down