load embeddings in parallel (#1352)

* load embeddings in parallel

* correctly capture unclipped

* test

* another test

* add convenient copy assets target

* cleanup
This commit is contained in:
Bruce Martin
2020-04-07 09:48:07 -07:00
committed by GitHub
parent 3b341a7191
commit bcacb75296
7 changed files with 83 additions and 32 deletions
+4
View File
@@ -42,6 +42,10 @@ build-cli: build-client
build-for-server-dev: clean-server build-client build-for-server-dev: clean-server build-client
$(call copy_client_assets,client/build,server) $(call copy_client_assets,client/build,server)
.PHONY: copy-client-assets
copy-client-assets:
$(call copy_client_assets,client/build,server)
# TESTING # TESTING
.PHONY: test .PHONY: test
test: unit-test smoke-test test: unit-test smoke-test
+23 -17
View File
@@ -1,6 +1,7 @@
import _ from "lodash"; import _ from "lodash";
import * as globals from "../globals"; import * as globals from "../globals";
import { Universe, MatrixFBS } from "../util/stateManager"; import { Universe, MatrixFBS } from "../util/stateManager";
import * as Dataframe from "../util/dataframe";
import { import {
catchErrorsWrap, catchErrorsWrap,
doJsonRequest, doJsonRequest,
@@ -74,24 +75,29 @@ function varAnnotationFetchAndLoad(dispatch, schema) {
/* /*
return promise fetching layout we need return promise fetching layout we need
*/ */
function layoutFetchAndLoad(dispatch) { function layoutFetchAndLoad(dispatch, schema) {
const embeddings = schema?.schema?.layout?.obs ?? [];
const embNames = embeddings.map(e => e.name);
const baseURL = `${globals.API.prefix}${globals.API.version}layout/obs`;
const plimit = new PromiseLimit(4);
return Promise.all( return Promise.all(
["layout/obs"] embNames.map(e =>
.map(path => { plimit.add(() => {
const url = `${globals.API.prefix}${globals.API.version}${path}`; const url = `${baseURL}?layout-name=${encodeURIComponent(e)}`;
return doBinaryRequest(url); return doBinaryRequest(url).then(buffer =>
Universe.matrixFBSToDataframe(buffer)
);
}) })
.map(rqst => rqst.then(buffer => Universe.matrixFBSToDataframe(buffer))) )
.map(resp => ).then(dfs => {
resp.then(df => const df = Dataframe.Dataframe.empty().withColsFromAll(dfs);
dispatch({ dispatch({
type: "universe: column load success", type: "universe: column load success",
dim: "obsLayout", dim: "obsLayout",
dataframe: df dataframe: df
}) });
) });
)
);
} }
/* /*
@@ -130,7 +136,7 @@ const doInitialDataLoad = () =>
Step 2 - load the minimum stuff required to display. Step 2 - load the minimum stuff required to display.
*/ */
await Promise.all([ await Promise.all([
layoutFetchAndLoad(dispatch), layoutFetchAndLoad(dispatch, schema),
varAnnotationFetchAndLoad(dispatch, schema) varAnnotationFetchAndLoad(dispatch, schema)
]); ]);
+17 -6
View File
@@ -39,14 +39,20 @@ const WorldReducer = (
/* incremental initial data load - always assumes world == universe */ /* incremental initial data load - always assumes world == universe */
const { universe } = nextSharedState; const { universe } = nextSharedState;
const { dim } = action; const { dim } = action;
// we don't clip anything except for varData and obsAnnotations
let unclipped = state.unclipped;
if (dim == "varData" || dim == "obsAnnotations") {
unclipped = {
...unclipped,
[dim]: universe[dim].clone()
};
}
return { return {
...state, ...state,
schema: universe.schema, schema: universe.schema,
[dim]: universe[dim].clone(), [dim]: universe[dim].clone(),
unclipped: { unclipped
...state.unclipped,
[dim]: universe[dim].clone()
}
}; };
} }
@@ -106,10 +112,15 @@ const WorldReducer = (
const { userDefinedGenes, diffexpGenes } = prevSharedState; const { userDefinedGenes, diffexpGenes } = prevSharedState;
const allTheGenesWeNeed = [ const allTheGenesWeNeed = [
...new Set( ...new Set(
[userDefinedGenes, diffexpGenes, Object.keys(action.expressionData)].filter(ele => ele).flat() [userDefinedGenes, diffexpGenes, Object.keys(action.expressionData)]
.filter(ele => ele)
.flat()
) )
]; ];
unclippedVarData = ControlsHelpers.pruneVarDataCache(unclippedVarData, allTheGenesWeNeed); unclippedVarData = ControlsHelpers.pruneVarDataCache(
unclippedVarData,
allTheGenesWeNeed
);
// at this point, we have the unclipped data in unclippedVarData. // at this point, we have the unclipped data in unclippedVarData.
// Now create clipped. // Now create clipped.
+9 -1
View File
@@ -10,6 +10,7 @@ from server.common.errors import (
PrepareError, PrepareError,
DisabledFeatureError, DisabledFeatureError,
ExceedsLimitError, ExceedsLimitError,
DatasetAccessError,
) )
import json import json
@@ -179,14 +180,21 @@ def diffexp_obs_post(request, data_adaptor):
def layout_obs_get(request, data_adaptor): def layout_obs_get(request, data_adaptor):
fields = request.args.getlist("layout-name", None)
num_columns_requested = len(data_adaptor.get_embedding_names()) if len(fields) == 0 else len(fields)
if data_adaptor.config.exceeds_limit("column_request_max", num_columns_requested):
return abort(HTTPStatus.BAD_REQUEST)
preferred_mimetype = request.accept_mimetypes.best_match(["application/octet-stream"]) preferred_mimetype = request.accept_mimetypes.best_match(["application/octet-stream"])
if preferred_mimetype != "application/octet-stream": if preferred_mimetype != "application/octet-stream":
return abort(HTTPStatus.NOT_ACCEPTABLE) return abort(HTTPStatus.NOT_ACCEPTABLE)
try: try:
return make_response( return make_response(
data_adaptor.layout_to_fbs_matrix(), HTTPStatus.OK, {"Content-Type": "application/octet-stream"} data_adaptor.layout_to_fbs_matrix(fields), HTTPStatus.OK, {"Content-Type": "application/octet-stream"}
) )
except (KeyError, DatasetAccessError) as e:
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
except PrepareError: except PrepareError:
return abort_and_log( return abort_and_log(
HTTPStatus.NOT_IMPLEMENTED, HTTPStatus.NOT_IMPLEMENTED,
+3 -5
View File
@@ -331,10 +331,9 @@ class DataAdaptor(metaclass=ABCMeta):
normalized_layout = normalized_layout.astype(dtype=np.float32) normalized_layout = normalized_layout.astype(dtype=np.float32)
return normalized_layout return normalized_layout
def layout_to_fbs_matrix(self): def layout_to_fbs_matrix(self, fields):
""" same as layout, except returns a flatbuffer """
""" """
return all embeddings as a flatbuffer, using the cellxgene matrix fbs encoding. return specified embeddings as a flatbuffer, using the cellxgene matrix fbs encoding.
* returns only first two dimensions, with name {ename}_0 and {ename}_1, * returns only first two dimensions, with name {ename}_0 and {ename}_1,
where {ename} is the embedding name. where {ename} is the embedding name.
@@ -343,8 +342,7 @@ class DataAdaptor(metaclass=ABCMeta):
* does not support filtering * does not support filtering
""" """
embeddings = self.get_embedding_names() if fields is None or len(fields) == 0 else fields
embeddings = self.get_embedding_names()
layout_data = [] layout_data = []
with ServerTiming.time(f"layout.query"): with ServerTiming.time(f"layout.query"):
for ename in embeddings: for ename in embeddings:
+10
View File
@@ -67,6 +67,16 @@
"name": "umap", "name": "umap",
"type": "float32", "type": "float32",
"dims": ["umap_0", "umap_1"] "dims": ["umap_0", "umap_1"]
},
{
"name": "tsne",
"type": "float32",
"dims": ["tsne_0", "tsne_1"]
},
{
"name": "pca",
"type": "float32",
"dims": ["pca_0", "pca_1"]
} }
] ]
} }
+17 -3
View File
@@ -34,7 +34,7 @@ Test the anndata adaptor using the pbmc3k data set.
class AdaptorTest(unittest.TestCase): class AdaptorTest(unittest.TestCase):
def setUp(self): def setUp(self):
args = { args = {
"embeddings__names": ["umap"], "embeddings__names": ["umap", "tsne", "pca"],
"presentation__max_categories": 100, "presentation__max_categories": 100,
"single_dataset__obs_names": None, "single_dataset__obs_names": None,
"single_dataset__var_names": None, "single_dataset__var_names": None,
@@ -122,9 +122,9 @@ class AdaptorTest(unittest.TestCase):
check_feature("PUT", "/annotations/obs", False) check_feature("PUT", "/annotations/obs", False)
def test_layout(self): def test_layout(self):
fbs = self.data.layout_to_fbs_matrix() fbs = self.data.layout_to_fbs_matrix(fields=None)
layout = decode_fbs.decode_matrix_FBS(fbs) layout = decode_fbs.decode_matrix_FBS(fbs)
self.assertEqual(layout["n_cols"], 2) self.assertEqual(layout["n_cols"], 6)
self.assertEqual(layout["n_rows"], 2638) self.assertEqual(layout["n_rows"], 2638)
X = layout["columns"][0] X = layout["columns"][0]
@@ -132,6 +132,20 @@ class AdaptorTest(unittest.TestCase):
Y = layout["columns"][1] Y = layout["columns"][1]
self.assertTrue((Y >= 0).all() and (Y <= 1).all()) self.assertTrue((Y >= 0).all() and (Y <= 1).all())
def test_layout_fields(self):
""" X_pca, X_tsne, X_umap are available """
fbs = self.data.layout_to_fbs_matrix(["pca"])
layout = decode_fbs.decode_matrix_FBS(fbs)
self.assertEqual(layout["n_cols"], 2)
self.assertEqual(layout["n_rows"], 2638)
self.assertCountEqual(layout["col_idx"], ["pca_0", "pca_1"])
fbs = self.data.layout_to_fbs_matrix(["tsne", "pca"])
layout = decode_fbs.decode_matrix_FBS(fbs)
self.assertEqual(layout["n_cols"], 4)
self.assertEqual(layout["n_rows"], 2638)
self.assertCountEqual(layout["col_idx"], ["tsne_0", "tsne_1", "pca_0", "pca_1"])
def test_annotations(self): def test_annotations(self):
fbs = self.data.annotation_to_fbs_matrix("obs") fbs = self.data.annotation_to_fbs_matrix("obs")
annotations = decode_fbs.decode_matrix_FBS(fbs) annotations = decode_fbs.decode_matrix_FBS(fbs)